aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--subprojects/frontend/package.json2
-rw-r--r--subprojects/frontend/src/Refinery.tsx140
-rw-r--r--subprojects/frontend/src/editor/EditorPane.tsx2
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts6
-rw-r--r--subprojects/frontend/src/graph/GraphPane.tsx28
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts5
-rw-r--r--subprojects/frontend/src/xtext/xtextMessages.ts6
-rw-r--r--subprojects/language-semantics/build.gradle.kts2
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java337
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/SemanticsUtils.java31
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTree.java20
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTreeCursor.java9
-rw-r--r--subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/tests/DecisionTreeTests.java11
-rw-r--r--subprojects/language-web/build.gradle.kts2
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsErrorResult.java9
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java11
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java155
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java13
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java2
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java6
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java2
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java12
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java36
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java8
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java16
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java2
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java4
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java4
-rw-r--r--yarn.lock23
29 files changed, 819 insertions, 85 deletions
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json
index 06ff9f6b..39ebd1df 100644
--- a/subprojects/frontend/package.json
+++ b/subprojects/frontend/package.json
@@ -49,6 +49,7 @@
49 "ansi-styles": "^6.2.1", 49 "ansi-styles": "^6.2.1",
50 "csstype": "^3.1.2", 50 "csstype": "^3.1.2",
51 "escape-string-regexp": "^5.0.0", 51 "escape-string-regexp": "^5.0.0",
52 "json-stringify-pretty-compact": "^4.0.0",
52 "lodash-es": "^4.17.21", 53 "lodash-es": "^4.17.21",
53 "loglevel": "^1.8.1", 54 "loglevel": "^1.8.1",
54 "loglevel-plugin-prefix": "^0.8.4", 55 "loglevel-plugin-prefix": "^0.8.4",
@@ -59,6 +60,7 @@
59 "notistack": "^3.0.1", 60 "notistack": "^3.0.1",
60 "react": "^18.2.0", 61 "react": "^18.2.0",
61 "react-dom": "^18.2.0", 62 "react-dom": "^18.2.0",
63 "react-resize-detector": "^9.1.0",
62 "xstate": "^4.38.2", 64 "xstate": "^4.38.2",
63 "zod": "^3.22.0" 65 "zod": "^3.22.0"
64 }, 66 },
diff --git a/subprojects/frontend/src/Refinery.tsx b/subprojects/frontend/src/Refinery.tsx
index b5ff94e1..099646f0 100644
--- a/subprojects/frontend/src/Refinery.tsx
+++ b/subprojects/frontend/src/Refinery.tsx
@@ -4,13 +4,151 @@
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
8import MoreVertIcon from '@mui/icons-material/MoreVert';
9import Box from '@mui/material/Box';
7import Grow from '@mui/material/Grow'; 10import Grow from '@mui/material/Grow';
8import Stack from '@mui/material/Stack'; 11import Stack from '@mui/material/Stack';
12import { alpha, useTheme } from '@mui/material/styles';
9import { SnackbarProvider } from 'notistack'; 13import { SnackbarProvider } from 'notistack';
14import { memo, useRef, useState } from 'react';
15import { useResizeDetector } from 'react-resize-detector';
10 16
11import TopBar from './TopBar'; 17import TopBar from './TopBar';
12import UpdateNotification from './UpdateNotification'; 18import UpdateNotification from './UpdateNotification';
13import EditorPane from './editor/EditorPane'; 19import EditorPane from './editor/EditorPane';
20import GraphPane from './graph/GraphPane';
21
22const DirectionalSplitPane = memo(function SplitPanel({
23 horizontalSplit,
24}: {
25 horizontalSplit: boolean;
26}): JSX.Element {
27 const theme = useTheme();
28 const stackRef = useRef<HTMLDivElement>(null);
29 const sliderRef = useRef<HTMLDivElement>(null);
30 const [resizing, setResizing] = useState(false);
31 const [fraction, setFraction] = useState(0.5);
32
33 const direction = horizontalSplit ? 'column' : 'row';
34 const axis = horizontalSplit ? 'height' : 'width';
35 const primarySize = `calc(${fraction * 100}% - 0.5px)`;
36 const secondarySize = `calc(${(1 - fraction) * 100}% - 0.5px)`;
37
38 return (
39 <Stack direction={direction} height="100%" overflow="hidden" ref={stackRef}>
40 <Box {...{ [axis]: primarySize }}>
41 <EditorPane />
42 </Box>
43 <Box
44 sx={{
45 overflow: 'visible',
46 position: 'relative',
47 [axis]: '0px',
48 display: 'flex',
49 flexDirection: direction,
50 [horizontalSplit
51 ? 'borderBottom'
52 : 'borderRight']: `1px solid ${theme.palette.outer.border}`,
53 }}
54 >
55 <Box
56 ref={sliderRef}
57 sx={{
58 display: 'flex',
59 position: 'absolute',
60 [axis]: theme.spacing(2),
61 ...(horizontalSplit
62 ? {
63 top: theme.spacing(-1),
64 left: 0,
65 right: 0,
66 transform: 'translateY(0.5px)',
67 }
68 : {
69 left: theme.spacing(-1),
70 top: 0,
71 bottom: 0,
72 transform: 'translateX(0.5px)',
73 }),
74 zIndex: 999,
75 alignItems: 'center',
76 justifyContent: 'center',
77 color: theme.palette.text.secondary,
78 cursor: horizontalSplit ? 'ns-resize' : 'ew-resize',
79 '.MuiSvgIcon-root': {
80 opacity: resizing ? 1 : 0,
81 },
82 ...(resizing
83 ? {
84 background: alpha(
85 theme.palette.text.primary,
86 theme.palette.action.activatedOpacity,
87 ),
88 }
89 : {
90 '&:hover': {
91 background: alpha(
92 theme.palette.text.primary,
93 theme.palette.action.hoverOpacity,
94 ),
95 '.MuiSvgIcon-root': {
96 opacity: 1,
97 },
98 },
99 }),
100 }}
101 onPointerDown={(event) => {
102 if (event.button !== 0) {
103 return;
104 }
105 sliderRef.current?.setPointerCapture(event.pointerId);
106 setResizing(true);
107 }}
108 onPointerUp={(event) => {
109 if (event.button !== 0) {
110 return;
111 }
112 sliderRef.current?.releasePointerCapture(event.pointerId);
113 setResizing(false);
114 }}
115 onPointerMove={(event) => {
116 if (!resizing) {
117 return;
118 }
119 const container = stackRef.current;
120 if (container === null) {
121 return;
122 }
123 const rect = container.getBoundingClientRect();
124 const newFraction = horizontalSplit
125 ? (event.clientY - rect.top) / rect.height
126 : (event.clientX - rect.left) / rect.width;
127 setFraction(Math.min(0.9, Math.max(0.1, newFraction)));
128 }}
129 onDoubleClick={() => setFraction(0.5)}
130 >
131 {horizontalSplit ? <MoreHorizIcon /> : <MoreVertIcon />}
132 </Box>
133 </Box>
134 <Box {...{ [axis]: secondarySize }}>
135 <GraphPane />
136 </Box>
137 </Stack>
138 );
139});
140
141function SplitPane(): JSX.Element {
142 const { ref, width, height } = useResizeDetector();
143 const horizontalSplit =
144 width !== undefined && height !== undefined && height > width;
145
146 return (
147 <Box height="100%" overflow="auto" ref={ref}>
148 <DirectionalSplitPane horizontalSplit={horizontalSplit} />
149 </Box>
150 );
151}
14 152
15export default function Refinery(): JSX.Element { 153export default function Refinery(): JSX.Element {
16 return ( 154 return (
@@ -18,7 +156,7 @@ export default function Refinery(): JSX.Element {
18 <UpdateNotification /> 156 <UpdateNotification />
19 <Stack direction="column" height="100%" overflow="auto"> 157 <Stack direction="column" height="100%" overflow="auto">
20 <TopBar /> 158 <TopBar />
21 <EditorPane /> 159 <SplitPane />
22 </Stack> 160 </Stack>
23 </SnackbarProvider> 161 </SnackbarProvider>
24 ); 162 );
diff --git a/subprojects/frontend/src/editor/EditorPane.tsx b/subprojects/frontend/src/editor/EditorPane.tsx
index 87f408fe..c9f86496 100644
--- a/subprojects/frontend/src/editor/EditorPane.tsx
+++ b/subprojects/frontend/src/editor/EditorPane.tsx
@@ -39,7 +39,7 @@ export default observer(function EditorPane(): JSX.Element {
39 const { editorStore } = useRootStore(); 39 const { editorStore } = useRootStore();
40 40
41 return ( 41 return (
42 <Stack direction="column" flexGrow={1} flexShrink={1} overflow="auto"> 42 <Stack direction="column" height="100%" overflow="auto">
43 <Toolbar variant="dense"> 43 <Toolbar variant="dense">
44 <EditorButtons editorStore={editorStore} /> 44 <EditorButtons editorStore={editorStore} />
45 </Toolbar> 45 </Toolbar>
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index b98f085e..c79f6ec1 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -58,6 +58,8 @@ export default class EditorStore {
58 58
59 disposed = false; 59 disposed = false;
60 60
61 semantics: unknown = {};
62
61 constructor(initialValue: string, pwaStore: PWAStore) { 63 constructor(initialValue: string, pwaStore: PWAStore) {
62 this.id = nanoid(); 64 this.id = nanoid();
63 this.state = createEditorState(initialValue, this); 65 this.state = createEditorState(initialValue, this);
@@ -282,6 +284,10 @@ export default class EditorStore {
282 return true; 284 return true;
283 } 285 }
284 286
287 setSemantics(semantics: unknown) {
288 this.semantics = semantics;
289 }
290
285 dispose(): void { 291 dispose(): void {
286 this.client?.dispose(); 292 this.client?.dispose();
287 this.disposed = true; 293 this.disposed = true;
diff --git a/subprojects/frontend/src/graph/GraphPane.tsx b/subprojects/frontend/src/graph/GraphPane.tsx
new file mode 100644
index 00000000..f69f52a6
--- /dev/null
+++ b/subprojects/frontend/src/graph/GraphPane.tsx
@@ -0,0 +1,28 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import Stack from '@mui/material/Stack';
8import { styled } from '@mui/material/styles';
9import stringify from 'json-stringify-pretty-compact';
10import { observer } from 'mobx-react-lite';
11
12import { useRootStore } from '../RootStoreProvider';
13
14const StyledCode = styled('code')(({ theme }) => ({
15 ...theme.typography.editor,
16 fontWeight: theme.typography.fontWeightEditorNormal,
17 margin: theme.spacing(2),
18 whiteSpace: 'pre',
19}));
20
21export default observer(function GraphPane(): JSX.Element {
22 const { editorStore } = useRootStore();
23 return (
24 <Stack direction="column" height="100%" overflow="auto">
25 <StyledCode>{stringify(editorStore?.semantics ?? {})}</StyledCode>
26 </Stack>
27 );
28});
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts
index abdf8518..d145cd30 100644
--- a/subprojects/frontend/src/xtext/XtextClient.ts
+++ b/subprojects/frontend/src/xtext/XtextClient.ts
@@ -38,7 +38,7 @@ export default class XtextClient {
38 private readonly occurrencesService: OccurrencesService; 38 private readonly occurrencesService: OccurrencesService;
39 39
40 constructor( 40 constructor(
41 store: EditorStore, 41 private readonly store: EditorStore,
42 private readonly pwaStore: PWAStore, 42 private readonly pwaStore: PWAStore,
43 ) { 43 ) {
44 this.webSocketClient = new XtextWebSocketClient( 44 this.webSocketClient = new XtextWebSocketClient(
@@ -114,6 +114,9 @@ export default class XtextClient {
114 case 'validate': 114 case 'validate':
115 this.validationService.onPush(push); 115 this.validationService.onPush(push);
116 return; 116 return;
117 case 'semantics':
118 this.store.setSemantics(push);
119 return;
117 default: 120 default:
118 throw new Error('Unknown service'); 121 throw new Error('Unknown service');
119 } 122 }
diff --git a/subprojects/frontend/src/xtext/xtextMessages.ts b/subprojects/frontend/src/xtext/xtextMessages.ts
index bbbff064..971720e1 100644
--- a/subprojects/frontend/src/xtext/xtextMessages.ts
+++ b/subprojects/frontend/src/xtext/xtextMessages.ts
@@ -34,7 +34,11 @@ export const XtextWebErrorResponse = z.object({
34 34
35export type XtextWebErrorResponse = z.infer<typeof XtextWebErrorResponse>; 35export type XtextWebErrorResponse = z.infer<typeof XtextWebErrorResponse>;
36 36
37export const XtextWebPushService = z.enum(['highlight', 'validate']); 37export const XtextWebPushService = z.enum([
38 'highlight',
39 'validate',
40 'semantics',
41]);
38 42
39export type XtextWebPushService = z.infer<typeof XtextWebPushService>; 43export type XtextWebPushService = z.infer<typeof XtextWebPushService>;
40 44
diff --git a/subprojects/language-semantics/build.gradle.kts b/subprojects/language-semantics/build.gradle.kts
index 38cd9e0d..23668f30 100644
--- a/subprojects/language-semantics/build.gradle.kts
+++ b/subprojects/language-semantics/build.gradle.kts
@@ -13,5 +13,7 @@ dependencies {
13 implementation(libs.eclipseCollections.api) 13 implementation(libs.eclipseCollections.api)
14 api(project(":refinery-language")) 14 api(project(":refinery-language"))
15 api(project(":refinery-store")) 15 api(project(":refinery-store"))
16 api(project(":refinery-store-query"))
17 api(project(":refinery-store-reasoning"))
16 testImplementation(testFixtures(project(":refinery-language"))) 18 testImplementation(testFixtures(project(":refinery-language")))
17} 19}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java
index fe67ed2c..93c7c8e5 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java
@@ -9,66 +9,298 @@ import com.google.inject.Inject;
9import org.eclipse.collections.api.factory.primitive.ObjectIntMaps; 9import org.eclipse.collections.api.factory.primitive.ObjectIntMaps;
10import org.eclipse.collections.api.map.primitive.MutableObjectIntMap; 10import org.eclipse.collections.api.map.primitive.MutableObjectIntMap;
11import tools.refinery.language.model.problem.*; 11import tools.refinery.language.model.problem.*;
12import tools.refinery.language.semantics.model.internal.DecisionTree;
13import tools.refinery.language.utils.BuiltinSymbols;
12import tools.refinery.language.utils.ProblemDesugarer; 14import tools.refinery.language.utils.ProblemDesugarer;
13import tools.refinery.store.representation.Symbol; 15import tools.refinery.language.utils.ProblemUtil;
16import tools.refinery.store.model.ModelStoreBuilder;
17import tools.refinery.store.reasoning.ReasoningAdapter;
18import tools.refinery.store.reasoning.representation.PartialRelation;
19import tools.refinery.store.reasoning.seed.ModelSeed;
20import tools.refinery.store.reasoning.seed.Seed;
21import tools.refinery.store.reasoning.translator.containment.ContainmentHierarchyTranslator;
22import tools.refinery.store.reasoning.translator.metamodel.Metamodel;
23import tools.refinery.store.reasoning.translator.metamodel.MetamodelBuilder;
24import tools.refinery.store.reasoning.translator.metamodel.MetamodelTranslator;
25import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator;
26import tools.refinery.store.reasoning.translator.multiplicity.ConstrainedMultiplicity;
27import tools.refinery.store.reasoning.translator.multiplicity.Multiplicity;
28import tools.refinery.store.reasoning.translator.multiplicity.UnconstrainedMultiplicity;
14import tools.refinery.store.representation.TruthValue; 29import tools.refinery.store.representation.TruthValue;
30import tools.refinery.store.representation.cardinality.CardinalityInterval;
31import tools.refinery.store.representation.cardinality.CardinalityIntervals;
32import tools.refinery.store.representation.cardinality.UpperCardinalities;
15import tools.refinery.store.tuple.Tuple; 33import tools.refinery.store.tuple.Tuple;
16 34
17import java.util.HashMap; 35import java.util.ArrayList;
36import java.util.LinkedHashMap;
18import java.util.Map; 37import java.util.Map;
19 38
20public class ModelInitializer { 39public class ModelInitializer {
21 @Inject 40 @Inject
22 private ProblemDesugarer desugarer; 41 private ProblemDesugarer desugarer;
23 42
43 @Inject
44 private SemanticsUtils semanticsUtils;
45
46 private Problem problem;
47
48 private BuiltinSymbols builtinSymbols;
49
50 private PartialRelation nodeRelation;
51
24 private final MutableObjectIntMap<Node> nodeTrace = ObjectIntMaps.mutable.empty(); 52 private final MutableObjectIntMap<Node> nodeTrace = ObjectIntMaps.mutable.empty();
25 53
26 private final Map<tools.refinery.language.model.problem.Relation, Symbol<TruthValue>> relationTrace = 54 private final Map<Relation, RelationInfo> relationInfoMap = new LinkedHashMap<>();
27 new HashMap<>();
28 55
29 private int nodeCount = 0; 56 private Map<Relation, PartialRelation> relationTrace;
57
58 private final MetamodelBuilder metamodelBuilder = Metamodel.builder();
59
60 public int getNodeCount() {
61 return nodeTrace.size();
62 }
63
64 public MutableObjectIntMap<Node> getNodeTrace() {
65 return nodeTrace;
66 }
30 67
31 /*public void createModel(Problem problem) { 68 public Map<Relation, PartialRelation> getRelationTrace() {
32 var builtinSymbols = desugarer.getBuiltinSymbols(problem).orElseThrow(() -> new IllegalArgumentException( 69 return relationTrace;
70 }
71
72 public ModelSeed createModel(Problem problem, ModelStoreBuilder builder) {
73 this.problem = problem;
74 builtinSymbols = desugarer.getBuiltinSymbols(problem).orElseThrow(() -> new IllegalArgumentException(
33 "Problem has no builtin library")); 75 "Problem has no builtin library"));
34 var collectedSymbols = desugarer.collectSymbols(problem); 76 var nodeInfo = collectPartialRelation(builtinSymbols.node(), 1, TruthValue.TRUE, TruthValue.TRUE);
35 for (var node : collectedSymbols.nodes().keySet()) { 77 nodeRelation = nodeInfo.partialRelation();
36 nodeTrace.put(node, nodeCount); 78 metamodelBuilder.type(nodeRelation, true);
37 nodeCount += 1; 79 relationInfoMap.put(builtinSymbols.exists(), new RelationInfo(ReasoningAdapter.EXISTS_SYMBOL, null,
38 } 80 TruthValue.TRUE));
39 for (var pair : collectedSymbols.relations().entrySet()) { 81 relationInfoMap.put(builtinSymbols.equals(), new RelationInfo(ReasoningAdapter.EQUALS_SYMBOL, (TruthValue) null,
40 var relation = pair.getKey(); 82 null));
41 var relationInfo = pair.getValue(); 83 relationInfoMap.put(builtinSymbols.contained(), new RelationInfo(ContainmentHierarchyTranslator.CONTAINED_SYMBOL,
42 var isEqualsRelation = relation == builtinSymbols.equals(); 84 null, TruthValue.UNKNOWN));
43 var decisionTree = mergeAssertions(relationInfo, isEqualsRelation); 85 relationInfoMap.put(builtinSymbols.contains(), new RelationInfo(ContainmentHierarchyTranslator.CONTAINS_SYMBOL,
44 var defaultValue = isEqualsRelation ? TruthValue.FALSE : TruthValue.UNKNOWN; 86 null, TruthValue.UNKNOWN));
45 relationTrace.put(relation, Symbol.of( 87 relationInfoMap.put(builtinSymbols.invalidNumberOfContainers(),
46 relationInfo.name(), relationInfo.arity(), TruthValue.class, defaultValue)); 88 new RelationInfo(ContainmentHierarchyTranslator.INVALID_NUMBER_OF_CONTAINERS, TruthValue.FALSE,
47 } 89 TruthValue.FALSE));
48 } 90 collectNodes();
49 91 collectPartialSymbols();
50 private DecisionTree mergeAssertions(RelationInfo relationInfo, boolean isEqualsRelation) { 92 collectAssertions();
51 var arity = relationInfo.arity(); 93 var metamodel = metamodelBuilder.build();
52 var defaultAssertions = new DecisionTree(arity, isEqualsRelation ? null : TruthValue.UNKNOWN); 94 builder.with(ReasoningAdapter.builder());
53 var assertions = new DecisionTree(arity); 95 builder.with(new MultiObjectTranslator());
54 for (var assertion : relationInfo.assertions()) { 96 builder.with(new MetamodelTranslator(metamodel));
55 var tuple = getTuple(assertion); 97 relationTrace = new LinkedHashMap<>(relationInfoMap.size());
56 var value = getTruthValue(assertion.getValue()); 98 int nodeCount = getNodeCount();
57 if (assertion.isDefault()) { 99 var modelSeedBuilder = ModelSeed.builder(nodeCount);
58 defaultAssertions.mergeValue(tuple, value); 100 for (var entry : relationInfoMap.entrySet()) {
101 var relation = entry.getKey();
102 var info = entry.getValue();
103 var partialRelation = info.partialRelation();
104 relationTrace.put(relation, partialRelation);
105 modelSeedBuilder.seed(partialRelation, info.toSeed(nodeCount));
106 }
107 return modelSeedBuilder.build();
108 }
109
110 private void collectNodes() {
111 for (var statement : problem.getStatements()) {
112 if (statement instanceof IndividualDeclaration individualDeclaration) {
113 for (var individual : individualDeclaration.getNodes()) {
114 collectNode(individual);
115 }
116 } else if (statement instanceof ClassDeclaration classDeclaration) {
117 var newNode = classDeclaration.getNewNode();
118 if (newNode != null) {
119 collectNode(newNode);
120 }
121 } else if (statement instanceof EnumDeclaration enumDeclaration) {
122 for (var literal : enumDeclaration.getLiterals()) {
123 collectNode(literal);
124 }
125 }
126 }
127 for (var node : problem.getNodes()) {
128 collectNode(node);
129 }
130 }
131
132 private void collectNode(Node node) {
133 nodeTrace.getIfAbsentPut(node, this::getNodeCount);
134 }
135
136 private void collectPartialSymbols() {
137 for (var statement : problem.getStatements()) {
138 if (statement instanceof ClassDeclaration classDeclaration) {
139 collectClassDeclaration(classDeclaration);
140 } else if (statement instanceof EnumDeclaration enumDeclaration) {
141 collectPartialRelation(enumDeclaration, 1, null, TruthValue.FALSE);
142 } else if (statement instanceof PredicateDefinition predicateDefinition) {
143 // TODO Implement predicate definitions
144 }
145 }
146 }
147
148 private void collectClassDeclaration(ClassDeclaration classDeclaration) {
149 collectPartialRelation(classDeclaration, 1, null, TruthValue.UNKNOWN);
150 for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) {
151 if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) {
152 collectPartialRelation(referenceDeclaration, 2, null, TruthValue.UNKNOWN);
153 var invalidMultiplicityConstraint = referenceDeclaration.getInvalidMultiplicity();
154 if (invalidMultiplicityConstraint != null) {
155 collectPartialRelation(invalidMultiplicityConstraint, 1, TruthValue.FALSE, TruthValue.FALSE);
156 }
59 } else { 157 } else {
60 assertions.mergeValue(tuple, value); 158 throw new IllegalArgumentException("Unknown feature declaration: " + featureDeclaration);
159 }
160 }
161 }
162
163 private RelationInfo collectPartialRelation(Relation relation, int arity, TruthValue value,
164 TruthValue defaultValue) {
165 return relationInfoMap.computeIfAbsent(relation, key -> {
166 var name = getName(relation);
167 return new RelationInfo(name, arity, value, defaultValue);
168 });
169 }
170
171 private String getName(Relation relation) {
172 return semanticsUtils.getName(relation).orElseGet(() -> "#" + relationInfoMap.size());
173 }
174
175 private void collectAssertions() {
176 for (var statement : problem.getStatements()) {
177 if (statement instanceof ClassDeclaration classDeclaration) {
178 collectClassDeclarationAssertions(classDeclaration);
179 } else if (statement instanceof EnumDeclaration enumDeclaration) {
180 collectEnumAssertions(enumDeclaration);
181 } else if (statement instanceof IndividualDeclaration individualDeclaration) {
182 for (var individual : individualDeclaration.getNodes()) {
183 collectIndividualAssertions(individual);
184 }
185 } else if (statement instanceof Assertion assertion) {
186 collectAssertion(assertion);
61 } 187 }
62 } 188 }
63 defaultAssertions.overwriteValues(assertions); 189 }
64 if (isEqualsRelation) { 190
65 for (int i = 0; i < nodeCount; i++) { 191 private void collectClassDeclarationAssertions(ClassDeclaration classDeclaration) {
66 defaultAssertions.setIfMissing(Tuple.of(i, i), TruthValue.TRUE); 192 var superTypes = classDeclaration.getSuperTypes();
193 var partialSuperTypes = new ArrayList<PartialRelation>(superTypes.size() + 1);
194 partialSuperTypes.add(nodeRelation);
195 for (var superType : superTypes) {
196 partialSuperTypes.add(getRelationInfo(superType).partialRelation());
197 }
198 var info = getRelationInfo(classDeclaration);
199 metamodelBuilder.type(info.partialRelation(), classDeclaration.isAbstract(),
200 partialSuperTypes);
201 var newNode = classDeclaration.getNewNode();
202 if (newNode != null) {
203 var newNodeId = getNodeId(newNode);
204 collectCardinalityAssertions(newNodeId, TruthValue.UNKNOWN);
205 mergeValue(classDeclaration, Tuple.of(newNodeId), TruthValue.TRUE);
206 }
207 for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) {
208 if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) {
209 collectReferenceDeclarationAssertions(classDeclaration, referenceDeclaration);
210 } else {
211 throw new IllegalArgumentException("Unknown feature declaration: " + featureDeclaration);
67 } 212 }
68 defaultAssertions.setAllMissing(TruthValue.FALSE);
69 } 213 }
70 return defaultAssertions; 214 }
71 }*/ 215
216 private void collectReferenceDeclarationAssertions(ClassDeclaration classDeclaration,
217 ReferenceDeclaration referenceDeclaration) {
218 var relation = getRelationInfo(referenceDeclaration).partialRelation();
219 var source = getRelationInfo(classDeclaration).partialRelation();
220 var target = getRelationInfo(referenceDeclaration.getReferenceType()).partialRelation();
221 boolean containment = referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT;
222 var opposite = referenceDeclaration.getOpposite();
223 PartialRelation oppositeRelation = null;
224 if (opposite != null) {
225 oppositeRelation = getRelationInfo(opposite).partialRelation();
226 }
227 var multiplicity = getMultiplicityConstraint(referenceDeclaration);
228 metamodelBuilder.reference(relation, source, containment, multiplicity, target, oppositeRelation);
229 }
230
231 private Multiplicity getMultiplicityConstraint(ReferenceDeclaration referenceDeclaration) {
232 if (!ProblemUtil.hasMultiplicityConstraint(referenceDeclaration)) {
233 return UnconstrainedMultiplicity.INSTANCE;
234 }
235 var problemMultiplicity = referenceDeclaration.getMultiplicity();
236 CardinalityInterval interval;
237 if (problemMultiplicity == null) {
238 interval = CardinalityIntervals.LONE;
239 } else if (problemMultiplicity instanceof ExactMultiplicity exactMultiplicity) {
240 interval = CardinalityIntervals.exactly(exactMultiplicity.getExactValue());
241 } else if (problemMultiplicity instanceof RangeMultiplicity rangeMultiplicity) {
242 var upperBound = rangeMultiplicity.getUpperBound();
243 interval = CardinalityIntervals.between(rangeMultiplicity.getLowerBound(),
244 upperBound < 0 ? UpperCardinalities.UNBOUNDED : UpperCardinalities.atMost(upperBound));
245 } else {
246 throw new IllegalArgumentException("Unknown multiplicity: " + problemMultiplicity);
247 }
248 var constraint = getRelationInfo(referenceDeclaration.getInvalidMultiplicity()).partialRelation();
249 return ConstrainedMultiplicity.of(interval, constraint);
250 }
251
252 private void collectEnumAssertions(EnumDeclaration enumDeclaration) {
253 var info = getRelationInfo(enumDeclaration);
254 metamodelBuilder.type(info.partialRelation(), nodeRelation);
255 var overlay = new DecisionTree(1, null);
256 for (var literal : enumDeclaration.getLiterals()) {
257 collectIndividualAssertions(literal);
258 var nodeId = getNodeId(literal);
259 overlay.mergeValue(Tuple.of(nodeId), TruthValue.TRUE);
260 }
261 info.assertions().overwriteValues(overlay);
262 }
263
264 private void collectIndividualAssertions(Node node) {
265 var nodeId = getNodeId(node);
266 collectCardinalityAssertions(nodeId, TruthValue.TRUE);
267 }
268
269 private void collectCardinalityAssertions(int nodeId, TruthValue value) {
270 mergeValue(builtinSymbols.exists(), Tuple.of(nodeId), value);
271 mergeValue(builtinSymbols.equals(), Tuple.of(nodeId, nodeId), value);
272 }
273
274 private void collectAssertion(Assertion assertion) {
275 var relation = assertion.getRelation();
276 var tuple = getTuple(assertion);
277 var value = getTruthValue(assertion.getValue());
278 if (assertion.isDefault()) {
279 mergeDefaultValue(relation, tuple, value);
280 } else {
281 mergeValue(relation, tuple, value);
282 }
283 }
284
285 private void mergeValue(Relation relation, Tuple key, TruthValue value) {
286 getRelationInfo(relation).assertions().mergeValue(key, value);
287 }
288
289 private void mergeDefaultValue(Relation relation, Tuple key, TruthValue value) {
290 getRelationInfo(relation).defaultAssertions().mergeValue(key, value);
291 }
292
293 private RelationInfo getRelationInfo(Relation relation) {
294 var info = relationInfoMap.get(relation);
295 if (info == null) {
296 throw new IllegalArgumentException("Unknown relation: " + relation);
297 }
298 return info;
299 }
300
301 private int getNodeId(Node node) {
302 return nodeTrace.getOrThrow(node);
303 }
72 304
73 private Tuple getTuple(Assertion assertion) { 305 private Tuple getTuple(Assertion assertion) {
74 var arguments = assertion.getArguments(); 306 var arguments = assertion.getArguments();
@@ -77,7 +309,7 @@ public class ModelInitializer {
77 for (int i = 0; i < arity; i++) { 309 for (int i = 0; i < arity; i++) {
78 var argument = arguments.get(i); 310 var argument = arguments.get(i);
79 if (argument instanceof NodeAssertionArgument nodeArgument) { 311 if (argument instanceof NodeAssertionArgument nodeArgument) {
80 nodes[i] = nodeTrace.getOrThrow(nodeArgument.getNode()); 312 nodes[i] = getNodeId(nodeArgument.getNode());
81 } else if (argument instanceof WildcardAssertionArgument) { 313 } else if (argument instanceof WildcardAssertionArgument) {
82 nodes[i] = -1; 314 nodes[i] = -1;
83 } else { 315 } else {
@@ -98,4 +330,27 @@ public class ModelInitializer {
98 case ERROR -> TruthValue.ERROR; 330 case ERROR -> TruthValue.ERROR;
99 }; 331 };
100 } 332 }
333
334 private record RelationInfo(PartialRelation partialRelation, DecisionTree assertions,
335 DecisionTree defaultAssertions) {
336 public RelationInfo(String name, int arity, TruthValue value, TruthValue defaultValue) {
337 this(new PartialRelation(name, arity), value, defaultValue);
338 }
339
340 public RelationInfo(PartialRelation partialRelation, TruthValue value, TruthValue defaultValue) {
341 this(partialRelation, new DecisionTree(partialRelation.arity(), value),
342 new DecisionTree(partialRelation.arity(), defaultValue));
343 }
344
345 public Seed<TruthValue> toSeed(int nodeCount) {
346 defaultAssertions.overwriteValues(assertions);
347 if (partialRelation.equals(ReasoningAdapter.EQUALS_SYMBOL)) {
348 for (int i = 0; i < nodeCount; i++) {
349 defaultAssertions.setIfMissing(Tuple.of(i, i), TruthValue.TRUE);
350 }
351 defaultAssertions.setAllMissing(TruthValue.FALSE);
352 }
353 return defaultAssertions;
354 }
355 }
101} 356}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/SemanticsUtils.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/SemanticsUtils.java
new file mode 100644
index 00000000..47c89e9b
--- /dev/null
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/SemanticsUtils.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.semantics.model;
7
8import com.google.inject.Inject;
9import com.google.inject.Singleton;
10import org.eclipse.emf.ecore.EObject;
11import org.eclipse.xtext.naming.IQualifiedNameConverter;
12import org.eclipse.xtext.naming.IQualifiedNameProvider;
13
14import java.util.Optional;
15
16@Singleton
17public class SemanticsUtils {
18 @Inject
19 private IQualifiedNameProvider qualifiedNameProvider;
20
21 @Inject
22 private IQualifiedNameConverter qualifiedNameConverter;
23
24 public Optional<String> getName(EObject eObject) {
25 var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(eObject);
26 if (qualifiedName == null) {
27 return Optional.empty();
28 }
29 return Optional.of(qualifiedNameConverter.toString(qualifiedName));
30 }
31}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTree.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTree.java
index c1afecf9..d693dec3 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTree.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTree.java
@@ -7,10 +7,11 @@ package tools.refinery.language.semantics.model.internal;
7 7
8import org.eclipse.collections.api.factory.primitive.IntObjectMaps; 8import org.eclipse.collections.api.factory.primitive.IntObjectMaps;
9import tools.refinery.store.map.Cursor; 9import tools.refinery.store.map.Cursor;
10import tools.refinery.store.reasoning.seed.Seed;
10import tools.refinery.store.tuple.Tuple; 11import tools.refinery.store.tuple.Tuple;
11import tools.refinery.store.representation.TruthValue; 12import tools.refinery.store.representation.TruthValue;
12 13
13public class DecisionTree { 14public class DecisionTree implements Seed<TruthValue> {
14 private final int levels; 15 private final int levels;
15 16
16 private final DecisionTreeNode root; 17 private final DecisionTreeNode root;
@@ -29,6 +30,22 @@ public class DecisionTree {
29 this(levels, null); 30 this(levels, null);
30 } 31 }
31 32
33 @Override
34 public int arity() {
35 return levels;
36 }
37
38 @Override
39 public Class<TruthValue> valueType() {
40 return TruthValue.class;
41 }
42
43 @Override
44 public TruthValue reducedValue() {
45 return root.getReducedValue().getTruthValue();
46 }
47
48 @Override
32 public TruthValue get(Tuple tuple) { 49 public TruthValue get(Tuple tuple) {
33 return root.getValue(levels - 1, tuple).getTruthValue(); 50 return root.getValue(levels - 1, tuple).getTruthValue();
34 } 51 }
@@ -60,6 +77,7 @@ public class DecisionTree {
60 return reducedValue == null ? null : reducedValue.getTruthValue(); 77 return reducedValue == null ? null : reducedValue.getTruthValue();
61 } 78 }
62 79
80 @Override
63 public Cursor<Tuple, TruthValue> getCursor(TruthValue defaultValue, int nodeCount) { 81 public Cursor<Tuple, TruthValue> getCursor(TruthValue defaultValue, int nodeCount) {
64 return new DecisionTreeCursor(levels, defaultValue, nodeCount, root); 82 return new DecisionTreeCursor(levels, defaultValue, nodeCount, root);
65 } 83 }
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTreeCursor.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTreeCursor.java
index 9a1e15a3..a9fc644a 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTreeCursor.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/internal/DecisionTreeCursor.java
@@ -67,6 +67,15 @@ class DecisionTreeCursor implements Cursor<Tuple, TruthValue> {
67 67
68 @Override 68 @Override
69 public boolean move() { 69 public boolean move() {
70 while (moveOne()) {
71 if (!value.equals(defaultValue)) {
72 return true;
73 }
74 }
75 return false;
76 }
77
78 private boolean moveOne() {
70 boolean found = false; 79 boolean found = false;
71 if (path.isEmpty() && !terminated) { 80 if (path.isEmpty() && !terminated) {
72 found = root.moveNext(levels - 1, this); 81 found = root.moveNext(levels - 1, this);
diff --git a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/tests/DecisionTreeTests.java b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/tests/DecisionTreeTests.java
index b3fcbabb..3c43d3bd 100644
--- a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/tests/DecisionTreeTests.java
+++ b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/tests/DecisionTreeTests.java
@@ -134,6 +134,17 @@ class DecisionTreeTests {
134 } 134 }
135 135
136 @Test 136 @Test
137 void overwriteIterationTest() {
138 var sut = new DecisionTree(1, TruthValue.TRUE);
139 var overwrite = new DecisionTree(1, null);
140 overwrite.mergeValue(Tuple.of(0), TruthValue.UNKNOWN);
141 sut.overwriteValues(overwrite);
142 var map = iterateAll(sut, TruthValue.UNKNOWN, 2);
143 assertThat(map.keySet(), hasSize(1));
144 assertThat(map, hasEntry(Tuple.of(1), TruthValue.TRUE));
145 }
146
147 @Test
137 void overwriteNothingTest() { 148 void overwriteNothingTest() {
138 var sut = new DecisionTree(2, TruthValue.UNKNOWN); 149 var sut = new DecisionTree(2, TruthValue.UNKNOWN);
139 var values = new DecisionTree(2, null); 150 var values = new DecisionTree(2, null);
diff --git a/subprojects/language-web/build.gradle.kts b/subprojects/language-web/build.gradle.kts
index 562a1bd9..20e5780b 100644
--- a/subprojects/language-web/build.gradle.kts
+++ b/subprojects/language-web/build.gradle.kts
@@ -17,6 +17,8 @@ val webapp: Configuration by configurations.creating {
17dependencies { 17dependencies {
18 implementation(project(":refinery-language")) 18 implementation(project(":refinery-language"))
19 implementation(project(":refinery-language-ide")) 19 implementation(project(":refinery-language-ide"))
20 implementation(project(":refinery-language-semantics"))
21 implementation(project(":refinery-store-query-viatra"))
20 implementation(libs.jetty.server) 22 implementation(libs.jetty.server)
21 implementation(libs.jetty.servlet) 23 implementation(libs.jetty.servlet)
22 implementation(libs.jetty.websocket.api) 24 implementation(libs.jetty.websocket.api)
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsErrorResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsErrorResult.java
new file mode 100644
index 00000000..ce34ef6c
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsErrorResult.java
@@ -0,0 +1,9 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8public record SemanticsErrorResult(String error) implements SemanticsResult {
9}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java
new file mode 100644
index 00000000..92639578
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java
@@ -0,0 +1,11 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import org.eclipse.xtext.web.server.IServiceResult;
9
10public sealed interface SemanticsResult extends IServiceResult permits SemanticsSuccessResult, SemanticsErrorResult {
11}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
new file mode 100644
index 00000000..483d24f6
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
@@ -0,0 +1,155 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import com.google.gson.JsonArray;
9import com.google.gson.JsonObject;
10import com.google.inject.Inject;
11import com.google.inject.Provider;
12import com.google.inject.Singleton;
13import org.eclipse.xtext.service.OperationCanceledManager;
14import org.eclipse.xtext.util.CancelIndicator;
15import org.eclipse.xtext.web.server.model.AbstractCachedService;
16import org.eclipse.xtext.web.server.model.IXtextWebDocument;
17import org.eclipse.xtext.web.server.model.XtextWebDocument;
18import org.eclipse.xtext.web.server.validation.ValidationService;
19import org.jetbrains.annotations.Nullable;
20import org.slf4j.Logger;
21import org.slf4j.LoggerFactory;
22import tools.refinery.language.model.problem.Problem;
23import tools.refinery.language.semantics.model.ModelInitializer;
24import tools.refinery.language.semantics.model.SemanticsUtils;
25import tools.refinery.store.model.Model;
26import tools.refinery.store.model.ModelStore;
27import tools.refinery.store.query.viatra.ViatraModelQueryAdapter;
28import tools.refinery.store.reasoning.ReasoningAdapter;
29import tools.refinery.store.reasoning.ReasoningStoreAdapter;
30import tools.refinery.store.reasoning.literal.Concreteness;
31import tools.refinery.store.reasoning.representation.PartialRelation;
32import tools.refinery.store.representation.TruthValue;
33import tools.refinery.store.tuple.Tuple;
34
35import java.util.Arrays;
36import java.util.List;
37import java.util.TreeMap;
38
39@Singleton
40public class SemanticsService extends AbstractCachedService<SemanticsResult> {
41 private static final Logger LOG = LoggerFactory.getLogger(SemanticsService.class);
42
43 @Inject
44 private SemanticsUtils semanticsUtils;
45
46 @Inject
47 private ValidationService validationService;
48
49 @Inject
50 private Provider<ModelInitializer> initializerProvider;
51
52 @Inject
53 private OperationCanceledManager operationCanceledManager;
54
55 @Override
56 public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) {
57 long start = System.currentTimeMillis();
58 Problem problem = getProblem(doc, cancelIndicator);
59 if (problem == null) {
60 return null;
61 }
62 var initializer = initializerProvider.get();
63 var builder = ModelStore.builder()
64 .with(ViatraModelQueryAdapter.builder());
65 operationCanceledManager.checkCanceled(cancelIndicator);
66 try {
67 var modelSeed = initializer.createModel(problem, builder);
68 operationCanceledManager.checkCanceled(cancelIndicator);
69 var nodeTrace = getNodeTrace(initializer);
70 operationCanceledManager.checkCanceled(cancelIndicator);
71 var store = builder.build();
72 operationCanceledManager.checkCanceled(cancelIndicator);
73 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed);
74 operationCanceledManager.checkCanceled(cancelIndicator);
75 var partialInterpretation = getPartialInterpretation(initializer, model, cancelIndicator);
76 long end = System.currentTimeMillis();
77 LOG.info("Computed semantics for {} ({}) in {}ms", doc.getResourceId(), doc.getStateId(), end - start);
78 return new SemanticsSuccessResult(nodeTrace, partialInterpretation);
79 } catch (RuntimeException e) {
80 LOG.error("Error while computing semantics", e);
81 return new SemanticsErrorResult(e.toString());
82 }
83 }
84
85 @Nullable
86 private Problem getProblem(IXtextWebDocument doc, CancelIndicator cancelIndicator) {
87 if (!(doc instanceof XtextWebDocument webDoc)) {
88 throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc);
89 }
90 var validationResult = webDoc.getCachedServiceResult(validationService, cancelIndicator, true);
91 boolean hasError = validationResult.getIssues().stream()
92 .anyMatch(issue -> "error".equals(issue.getSeverity()));
93 if (hasError) {
94 return null;
95 }
96 var contents = doc.getResource().getContents();
97 if (contents.isEmpty()) {
98 return null;
99 }
100 var model = contents.get(0);
101 if (!(model instanceof Problem problem)) {
102 return null;
103 }
104 return problem;
105 }
106
107 private List<String> getNodeTrace(ModelInitializer initializer) {
108 var nodeTrace = new String[initializer.getNodeCount()];
109 for (var entry : initializer.getNodeTrace().keyValuesView()) {
110 var node = entry.getOne();
111 var index = entry.getTwo();
112 nodeTrace[index] = semanticsUtils.getName(node).orElse(null);
113 }
114 return Arrays.asList(nodeTrace);
115 }
116
117 private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model,
118 CancelIndicator cancelIndicator) {
119 var adapter = model.getAdapter(ReasoningAdapter.class);
120 var json = new JsonObject();
121 for (var entry : initializer.getRelationTrace().entrySet()) {
122 var relation = entry.getKey();
123 var partialSymbol = entry.getValue();
124 var tuples = getTuplesJson(adapter, partialSymbol);
125 var name = semanticsUtils.getName(relation).orElse(partialSymbol.name());
126 json.add(name, tuples);
127 operationCanceledManager.checkCanceled(cancelIndicator);
128 }
129 return json;
130 }
131
132 private static JsonArray getTuplesJson(ReasoningAdapter adapter, PartialRelation partialSymbol) {
133 var interpretation = adapter.getPartialInterpretation(Concreteness.PARTIAL, partialSymbol);
134 var cursor = interpretation.getAll();
135 var map = new TreeMap<Tuple, TruthValue>();
136 while (cursor.move()) {
137 map.put(cursor.getKey(), cursor.getValue());
138 }
139 var tuples = new JsonArray();
140 for (var entry : map.entrySet()) {
141 tuples.add(toArray(entry.getKey(), entry.getValue()));
142 }
143 return tuples;
144 }
145
146 private static JsonArray toArray(Tuple tuple, TruthValue value) {
147 int arity = tuple.getSize();
148 var json = new JsonArray(arity + 1);
149 for (int i = 0; i < arity; i++) {
150 json.add(tuple.get(i));
151 }
152 json.add(value.toString());
153 return json;
154 }
155}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java
new file mode 100644
index 00000000..15fd4b55
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java
@@ -0,0 +1,13 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import com.google.gson.JsonObject;
9
10import java.util.List;
11
12public record SemanticsSuccessResult(List<String> nodes, JsonObject partialInterpretation) implements SemanticsResult {
13}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
index 0135d8f5..2c0e9329 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
@@ -134,7 +134,7 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
134 * @throws UnknownLanguageException if the Xtext language cannot be determined 134 * @throws UnknownLanguageException if the Xtext language cannot be determined
135 */ 135 */
136 protected Injector getInjector(IServiceContext context) { 136 protected Injector getInjector(IServiceContext context) {
137 IResourceServiceProvider resourceServiceProvider = null; 137 IResourceServiceProvider resourceServiceProvider;
138 var resourceName = context.getParameter("resource"); 138 var resourceName = context.getParameter("resource");
139 if (resourceName == null) { 139 if (resourceName == null) {
140 resourceName = ""; 140 resourceName = "";
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java
index 73527ee5..c3379329 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java
@@ -5,12 +5,11 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.message; 6package tools.refinery.language.web.xtext.server.message;
7 7
8import java.util.Objects; 8import com.google.gson.annotations.SerializedName;
9
10import org.eclipse.xtext.web.server.IServiceResult; 9import org.eclipse.xtext.web.server.IServiceResult;
11import org.eclipse.xtext.web.server.IUnwrappableServiceResult; 10import org.eclipse.xtext.web.server.IUnwrappableServiceResult;
12 11
13import com.google.gson.annotations.SerializedName; 12import java.util.Objects;
14 13
15public final class XtextWebOkResponse implements XtextWebResponse { 14public final class XtextWebOkResponse implements XtextWebResponse {
16 private String id; 15 private String id;
@@ -19,7 +18,6 @@ public final class XtextWebOkResponse implements XtextWebResponse {
19 private Object responseData; 18 private Object responseData;
20 19
21 public XtextWebOkResponse(String id, Object responseData) { 20 public XtextWebOkResponse(String id, Object responseData) {
22 super();
23 this.id = id; 21 this.id = id;
24 this.responseData = responseData; 22 this.responseData = responseData;
25 } 23 }
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java
index 61444c99..c370fb56 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java
@@ -5,5 +5,5 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.message; 6package tools.refinery.language.web.xtext.server.message;
7 7
8public sealed interface XtextWebResponse permits XtextWebOkResponse,XtextWebErrorResponse,XtextWebPushMessage { 8public sealed interface XtextWebResponse permits XtextWebOkResponse, XtextWebErrorResponse, XtextWebPushMessage {
9} 9}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java
index 4c9135c8..d4a8c433 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java
@@ -5,16 +5,28 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.push; 6package tools.refinery.language.web.xtext.server.push;
7 7
8import com.google.inject.Inject;
8import org.eclipse.xtext.web.server.IServiceContext; 9import org.eclipse.xtext.web.server.IServiceContext;
9import org.eclipse.xtext.web.server.XtextServiceDispatcher; 10import org.eclipse.xtext.web.server.XtextServiceDispatcher;
11import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry;
10import org.eclipse.xtext.web.server.model.XtextWebDocument; 12import org.eclipse.xtext.web.server.model.XtextWebDocument;
11 13
12import com.google.inject.Singleton; 14import com.google.inject.Singleton;
13 15
16import tools.refinery.language.web.semantics.SemanticsService;
14import tools.refinery.language.web.xtext.server.SubscribingServiceContext; 17import tools.refinery.language.web.xtext.server.SubscribingServiceContext;
15 18
16@Singleton 19@Singleton
17public class PushServiceDispatcher extends XtextServiceDispatcher { 20public class PushServiceDispatcher extends XtextServiceDispatcher {
21 @Inject
22 private SemanticsService semanticsService;
23
24 @Override
25 @Inject
26 protected void registerPreComputedServices(PrecomputedServiceRegistry registry) {
27 super.registerPreComputedServices(registry);
28 registry.addPrecomputedService(semanticsService);
29 }
18 30
19 @Override 31 @Override
20 protected XtextWebDocument getFullTextDocument(String fullText, String resourceId, IServiceContext context) { 32 protected XtextWebDocument getFullTextDocument(String fullText, String resourceId, IServiceContext context) {
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
index 56fd12c9..dfbd4878 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
@@ -5,11 +5,7 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.push; 6package tools.refinery.language.web.xtext.server.push;
7 7
8import java.util.ArrayList; 8import com.google.common.collect.ImmutableList;
9import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12
13import org.eclipse.xtext.util.CancelIndicator; 9import org.eclipse.xtext.util.CancelIndicator;
14import org.eclipse.xtext.web.server.IServiceResult; 10import org.eclipse.xtext.web.server.IServiceResult;
15import org.eclipse.xtext.web.server.model.AbstractCachedService; 11import org.eclipse.xtext.web.server.model.AbstractCachedService;
@@ -17,11 +13,13 @@ import org.eclipse.xtext.web.server.model.DocumentSynchronizer;
17import org.eclipse.xtext.web.server.model.XtextWebDocument; 13import org.eclipse.xtext.web.server.model.XtextWebDocument;
18import org.slf4j.Logger; 14import org.slf4j.Logger;
19import org.slf4j.LoggerFactory; 15import org.slf4j.LoggerFactory;
20
21import com.google.common.collect.ImmutableList;
22
23import tools.refinery.language.web.xtext.server.ResponseHandlerException; 16import tools.refinery.language.web.xtext.server.ResponseHandlerException;
24 17
18import java.util.ArrayList;
19import java.util.HashMap;
20import java.util.List;
21import java.util.Map;
22
25public class PushWebDocument extends XtextWebDocument { 23public class PushWebDocument extends XtextWebDocument {
26 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class); 24 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class);
27 25
@@ -36,37 +34,31 @@ public class PushWebDocument extends XtextWebDocument {
36 } 34 }
37 } 35 }
38 36
39 public boolean addPrecomputationListener(PrecomputationListener listener) { 37 public void addPrecomputationListener(PrecomputationListener listener) {
40 synchronized (precomputationListeners) { 38 synchronized (precomputationListeners) {
41 if (precomputationListeners.contains(listener)) { 39 if (precomputationListeners.contains(listener)) {
42 return false; 40 return;
43 } 41 }
44 precomputationListeners.add(listener); 42 precomputationListeners.add(listener);
45 listener.onSubscribeToPrecomputationEvents(getResourceId(), this); 43 listener.onSubscribeToPrecomputationEvents(getResourceId(), this);
46 return true;
47 } 44 }
48 } 45 }
49 46
50 public boolean removePrecomputationListener(PrecomputationListener listener) { 47 public void removePrecomputationListener(PrecomputationListener listener) {
51 synchronized (precomputationListeners) { 48 synchronized (precomputationListeners) {
52 return precomputationListeners.remove(listener); 49 precomputationListeners.remove(listener);
53 } 50 }
54 } 51 }
55 52
56 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, 53 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName,
57 CancelIndicator cancelIndicator, boolean logCacheMiss) { 54 CancelIndicator cancelIndicator, boolean logCacheMiss) {
58 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss);
59 if (result == null) {
60 LOG.error("{} service returned null result", serviceName);
61 return;
62 }
63 var serviceClass = service.getClass(); 55 var serviceClass = service.getClass();
64 var previousResult = precomputedServices.get(serviceClass); 56 var previousResult = precomputedServices.get(serviceClass);
65 if (previousResult != null && previousResult.equals(result)) { 57 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss);
66 return;
67 }
68 precomputedServices.put(serviceClass, result); 58 precomputedServices.put(serviceClass, result);
69 notifyPrecomputationListeners(serviceName, result); 59 if (result != null && !result.equals(previousResult)) {
60 notifyPrecomputationListeners(serviceName, result);
61 }
70 } 62 }
71 63
72 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { 64 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) {
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java
index d9e548cd..c72e8e67 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java
@@ -18,6 +18,7 @@ import org.eclipse.xtext.web.server.syntaxcoloring.HighlightingService;
18import org.eclipse.xtext.web.server.validation.ValidationService; 18import org.eclipse.xtext.web.server.validation.ValidationService;
19 19
20import com.google.inject.Inject; 20import com.google.inject.Inject;
21import tools.refinery.language.web.semantics.SemanticsService;
21 22
22public class PushWebDocumentAccess extends XtextWebDocumentAccess { 23public class PushWebDocumentAccess extends XtextWebDocumentAccess {
23 24
@@ -49,7 +50,7 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess {
49 precomputeServiceResult(service, false); 50 precomputeServiceResult(service, false);
50 } 51 }
51 } 52 }
52 53
53 protected <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, boolean logCacheMiss) { 54 protected <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, boolean logCacheMiss) {
54 var serviceName = getPrecomputedServiceName(service); 55 var serviceName = getPrecomputedServiceName(service);
55 readOnly(new CancelableUnitOfWork<Void, IXtextWebDocument>() { 56 readOnly(new CancelableUnitOfWork<Void, IXtextWebDocument>() {
@@ -60,7 +61,7 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess {
60 } 61 }
61 }); 62 });
62 } 63 }
63 64
64 protected String getPrecomputedServiceName(AbstractCachedService<? extends IServiceResult> service) { 65 protected String getPrecomputedServiceName(AbstractCachedService<? extends IServiceResult> service) {
65 if (service instanceof ValidationService) { 66 if (service instanceof ValidationService) {
66 return "validate"; 67 return "validate";
@@ -68,6 +69,9 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess {
68 if (service instanceof HighlightingService) { 69 if (service instanceof HighlightingService) {
69 return "highlight"; 70 return "highlight";
70 } 71 }
72 if (service instanceof SemanticsService) {
73 return "semantics";
74 }
71 throw new IllegalArgumentException("Unknown precomputed service: " + service); 75 throw new IllegalArgumentException("Unknown precomputed service: " + service);
72 } 76 }
73} 77}
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
index 927eeab1..99ca5420 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
@@ -93,7 +93,7 @@ class ProblemWebSocketServletIntegrationTest {
93 clientSocket.waitForTestResult(); 93 clientSocket.waitForTestResult();
94 assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); 94 assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL));
95 var responses = clientSocket.getResponses(); 95 var responses = clientSocket.getResponses();
96 assertThat(responses, hasSize(5)); 96 assertThat(responses, hasSize(7));
97 assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); 97 assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}"));
98 assertThat(responses.get(1), startsWith( 98 assertThat(responses.get(1), startsWith(
99 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\"," + 99 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\"," +
@@ -101,10 +101,16 @@ class ProblemWebSocketServletIntegrationTest {
101 assertThat(responses.get(2), equalTo( 101 assertThat(responses.get(2), equalTo(
102 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"validate\"," + 102 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"validate\"," +
103 "\"push\":{\"issues\":[]}}")); 103 "\"push\":{\"issues\":[]}}"));
104 assertThat(responses.get(3), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}")); 104 assertThat(responses.get(3), startsWith(
105 assertThat(responses.get(4), startsWith( 105 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"semantics\"," +
106 "\"push\":{"));
107 assertThat(responses.get(4), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}"));
108 assertThat(responses.get(5), startsWith(
106 "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\"," + 109 "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\"," +
107 "\"push\":{\"regions\":[")); 110 "\"push\":{\"regions\":["));
111 assertThat(responses.get(6), startsWith(
112 "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"semantics\"," +
113 "\"push\":{"));
108 } 114 }
109 115
110 @WebSocket 116 @WebSocket
@@ -117,14 +123,14 @@ class ProblemWebSocketServletIntegrationTest {
117 "\"fullText\":\"class Person.\n\"}}", 123 "\"fullText\":\"class Person.\n\"}}",
118 Callback.NOOP 124 Callback.NOOP
119 ); 125 );
120 case 3 -> //noinspection TextBlockMigration 126 case 4 -> //noinspection TextBlockMigration
121 session.sendText( 127 session.sendText(
122 "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\"," + 128 "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\"," +
123 "\"requiredStateId\":\"-80000000\",\"deltaText\":\"indiv q.\nnode(q).\n\"," + 129 "\"requiredStateId\":\"-80000000\",\"deltaText\":\"indiv q.\nnode(q).\n\"," +
124 "\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}", 130 "\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}",
125 Callback.NOOP 131 Callback.NOOP
126 ); 132 );
127 case 5 -> session.close(); 133 case 7 -> session.close();
128 } 134 }
129 } 135 }
130 } 136 }
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
index 09079aa8..991ff114 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
@@ -35,7 +35,7 @@ public class RestartableCachedThreadPool implements ExecutorService {
35 public void waitForTermination() { 35 public void waitForTermination() {
36 boolean result = false; 36 boolean result = false;
37 try { 37 try {
38 result = delegate.awaitTermination(1, TimeUnit.SECONDS); 38 result = delegate.awaitTermination(10, TimeUnit.SECONDS);
39 } catch (InterruptedException e) { 39 } catch (InterruptedException e) {
40 LOG.warn("Interrupted while waiting for delegate executor to stop", e); 40 LOG.warn("Interrupted while waiting for delegate executor to stop", e);
41 } 41 }
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java
index 841bacd3..b7142506 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java
@@ -95,7 +95,7 @@ class TransactionExecutorTest {
95 "0"))); 95 "0")));
96 96
97 var captor = newCaptor(); 97 var captor = newCaptor();
98 verify(responseHandler, times(2)).onResponse(captor.capture()); 98 verify(responseHandler, times(3)).onResponse(captor.capture());
99 var newStateId = getStateId("bar", captor.getAllValues().get(0)); 99 var newStateId = getStateId("bar", captor.getAllValues().get(0));
100 assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); 100 assertHighlightingResponse(newStateId, captor.getAllValues().get(1));
101 } 101 }
@@ -126,7 +126,7 @@ class TransactionExecutorTest {
126 private String updateFullText(ArgumentCaptor<XtextWebResponse> captor) throws ResponseHandlerException { 126 private String updateFullText(ArgumentCaptor<XtextWebResponse> captor) throws ResponseHandlerException {
127 var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", UPDATE_FULL_TEXT_PARAMS)); 127 var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", UPDATE_FULL_TEXT_PARAMS));
128 128
129 verify(responseHandler, times(3)).onResponse(captor.capture()); 129 verify(responseHandler, times(4)).onResponse(captor.capture());
130 return getStateId("foo", captor.getAllValues().get(0)); 130 return getStateId("foo", captor.getAllValues().get(0));
131 } 131 }
132 132
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 bac274b0..03b0c729 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
@@ -66,6 +66,10 @@ public final class ProblemUtil {
66 } 66 }
67 67
68 public static boolean hasMultiplicityConstraint(ReferenceDeclaration referenceDeclaration) { 68 public static boolean hasMultiplicityConstraint(ReferenceDeclaration referenceDeclaration) {
69 var opposite = referenceDeclaration.getOpposite();
70 if (opposite != null && opposite.getKind() == ReferenceKind.CONTAINMENT) {
71 return false;
72 }
69 var multiplicity = referenceDeclaration.getMultiplicity(); 73 var multiplicity = referenceDeclaration.getMultiplicity();
70 if (multiplicity instanceof UnboundedMultiplicity) { 74 if (multiplicity instanceof UnboundedMultiplicity) {
71 return false; 75 return false;
diff --git a/yarn.lock b/yarn.lock
index 607cb0b3..bb806e86 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2144,6 +2144,7 @@ __metadata:
2144 eslint-plugin-react: "npm:^7.33.1" 2144 eslint-plugin-react: "npm:^7.33.1"
2145 eslint-plugin-react-hooks: "npm:^4.6.0" 2145 eslint-plugin-react-hooks: "npm:^4.6.0"
2146 html-minifier-terser: "npm:^7.2.0" 2146 html-minifier-terser: "npm:^7.2.0"
2147 json-stringify-pretty-compact: "npm:^4.0.0"
2147 lodash-es: "npm:^4.17.21" 2148 lodash-es: "npm:^4.17.21"
2148 loglevel: "npm:^1.8.1" 2149 loglevel: "npm:^1.8.1"
2149 loglevel-plugin-prefix: "npm:^0.8.4" 2150 loglevel-plugin-prefix: "npm:^0.8.4"
@@ -2156,6 +2157,7 @@ __metadata:
2156 prettier: "npm:^3.0.1" 2157 prettier: "npm:^3.0.1"
2157 react: "npm:^18.2.0" 2158 react: "npm:^18.2.0"
2158 react-dom: "npm:^18.2.0" 2159 react-dom: "npm:^18.2.0"
2160 react-resize-detector: "npm:^9.1.0"
2159 typescript: "npm:5.1.6" 2161 typescript: "npm:5.1.6"
2160 vite: "npm:^4.4.9" 2162 vite: "npm:^4.4.9"
2161 vite-plugin-pwa: "npm:^0.16.4" 2163 vite-plugin-pwa: "npm:^0.16.4"
@@ -5206,6 +5208,13 @@ __metadata:
5206 languageName: node 5208 languageName: node
5207 linkType: hard 5209 linkType: hard
5208 5210
5211"json-stringify-pretty-compact@npm:^4.0.0":
5212 version: 4.0.0
5213 resolution: "json-stringify-pretty-compact@npm:4.0.0"
5214 checksum: 505781b4be7c72047ae8dfa667b520d20461ceac451b6516cb8ac5e12a758fbd7491d99d5e3f7e60423ce9d26ed4e4bcaccab3420bf651298901635c849017cf
5215 languageName: node
5216 linkType: hard
5217
5209"json5@npm:^1.0.2": 5218"json5@npm:^1.0.2":
5210 version: 1.0.2 5219 version: 1.0.2
5211 resolution: "json5@npm:1.0.2" 5220 resolution: "json5@npm:1.0.2"
@@ -5335,7 +5344,7 @@ __metadata:
5335 languageName: node 5344 languageName: node
5336 linkType: hard 5345 linkType: hard
5337 5346
5338"lodash@npm:^4.17.20": 5347"lodash@npm:^4.17.20, lodash@npm:^4.17.21":
5339 version: 4.17.21 5348 version: 4.17.21
5340 resolution: "lodash@npm:4.17.21" 5349 resolution: "lodash@npm:4.17.21"
5341 checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c 5350 checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c
@@ -6130,6 +6139,18 @@ __metadata:
6130 languageName: node 6139 languageName: node
6131 linkType: hard 6140 linkType: hard
6132 6141
6142"react-resize-detector@npm:^9.1.0":
6143 version: 9.1.0
6144 resolution: "react-resize-detector@npm:9.1.0"
6145 dependencies:
6146 lodash: "npm:^4.17.21"
6147 peerDependencies:
6148 react: ^16.0.0 || ^17.0.0 || ^18.0.0
6149 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
6150 checksum: ebd45b54ce68368a8d8b6b32ff3b7949e8f56d5cce21fb18b5044612b16ae9adfdd2eefa6494a700aaf6c64b2cec4c3c33d86c464d1772042dbb4d87dd3dafc6
6151 languageName: node
6152 linkType: hard
6153
6133"react-transition-group@npm:^4.4.5": 6154"react-transition-group@npm:^4.4.5":
6134 version: 4.4.5 6155 version: 4.4.5
6135 resolution: "react-transition-group@npm:4.4.5" 6156 resolution: "react-transition-group@npm:4.4.5"