diff options
author | Kristóf Marussy <kristof@marussy.com> | 2023-08-17 02:32:26 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2023-08-17 02:43:55 +0200 |
commit | eb5da232b5954895b449957c73e35d0b36e3a902 (patch) | |
tree | a8714116cfe3102659e9c5e5b90131e7ec248492 | |
parent | chore(deps): bump dependencies (diff) | |
download | refinery-eb5da232b5954895b449957c73e35d0b36e3a902.tar.gz refinery-eb5da232b5954895b449957c73e35d0b36e3a902.tar.zst refinery-eb5da232b5954895b449957c73e35d0b36e3a902.zip |
feat: basic semantics mapping and visualization
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 | ||
7 | import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; | ||
8 | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||
9 | import Box from '@mui/material/Box'; | ||
7 | import Grow from '@mui/material/Grow'; | 10 | import Grow from '@mui/material/Grow'; |
8 | import Stack from '@mui/material/Stack'; | 11 | import Stack from '@mui/material/Stack'; |
12 | import { alpha, useTheme } from '@mui/material/styles'; | ||
9 | import { SnackbarProvider } from 'notistack'; | 13 | import { SnackbarProvider } from 'notistack'; |
14 | import { memo, useRef, useState } from 'react'; | ||
15 | import { useResizeDetector } from 'react-resize-detector'; | ||
10 | 16 | ||
11 | import TopBar from './TopBar'; | 17 | import TopBar from './TopBar'; |
12 | import UpdateNotification from './UpdateNotification'; | 18 | import UpdateNotification from './UpdateNotification'; |
13 | import EditorPane from './editor/EditorPane'; | 19 | import EditorPane from './editor/EditorPane'; |
20 | import GraphPane from './graph/GraphPane'; | ||
21 | |||
22 | const 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 | |||
141 | function 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 | ||
15 | export default function Refinery(): JSX.Element { | 153 | export 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 | |||
7 | import Stack from '@mui/material/Stack'; | ||
8 | import { styled } from '@mui/material/styles'; | ||
9 | import stringify from 'json-stringify-pretty-compact'; | ||
10 | import { observer } from 'mobx-react-lite'; | ||
11 | |||
12 | import { useRootStore } from '../RootStoreProvider'; | ||
13 | |||
14 | const StyledCode = styled('code')(({ theme }) => ({ | ||
15 | ...theme.typography.editor, | ||
16 | fontWeight: theme.typography.fontWeightEditorNormal, | ||
17 | margin: theme.spacing(2), | ||
18 | whiteSpace: 'pre', | ||
19 | })); | ||
20 | |||
21 | export 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 | ||
35 | export type XtextWebErrorResponse = z.infer<typeof XtextWebErrorResponse>; | 35 | export type XtextWebErrorResponse = z.infer<typeof XtextWebErrorResponse>; |
36 | 36 | ||
37 | export const XtextWebPushService = z.enum(['highlight', 'validate']); | 37 | export const XtextWebPushService = z.enum([ |
38 | 'highlight', | ||
39 | 'validate', | ||
40 | 'semantics', | ||
41 | ]); | ||
38 | 42 | ||
39 | export type XtextWebPushService = z.infer<typeof XtextWebPushService>; | 43 | export 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; | |||
9 | import org.eclipse.collections.api.factory.primitive.ObjectIntMaps; | 9 | import org.eclipse.collections.api.factory.primitive.ObjectIntMaps; |
10 | import org.eclipse.collections.api.map.primitive.MutableObjectIntMap; | 10 | import org.eclipse.collections.api.map.primitive.MutableObjectIntMap; |
11 | import tools.refinery.language.model.problem.*; | 11 | import tools.refinery.language.model.problem.*; |
12 | import tools.refinery.language.semantics.model.internal.DecisionTree; | ||
13 | import tools.refinery.language.utils.BuiltinSymbols; | ||
12 | import tools.refinery.language.utils.ProblemDesugarer; | 14 | import tools.refinery.language.utils.ProblemDesugarer; |
13 | import tools.refinery.store.representation.Symbol; | 15 | import tools.refinery.language.utils.ProblemUtil; |
16 | import tools.refinery.store.model.ModelStoreBuilder; | ||
17 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
18 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
19 | import tools.refinery.store.reasoning.seed.ModelSeed; | ||
20 | import tools.refinery.store.reasoning.seed.Seed; | ||
21 | import tools.refinery.store.reasoning.translator.containment.ContainmentHierarchyTranslator; | ||
22 | import tools.refinery.store.reasoning.translator.metamodel.Metamodel; | ||
23 | import tools.refinery.store.reasoning.translator.metamodel.MetamodelBuilder; | ||
24 | import tools.refinery.store.reasoning.translator.metamodel.MetamodelTranslator; | ||
25 | import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; | ||
26 | import tools.refinery.store.reasoning.translator.multiplicity.ConstrainedMultiplicity; | ||
27 | import tools.refinery.store.reasoning.translator.multiplicity.Multiplicity; | ||
28 | import tools.refinery.store.reasoning.translator.multiplicity.UnconstrainedMultiplicity; | ||
14 | import tools.refinery.store.representation.TruthValue; | 29 | import tools.refinery.store.representation.TruthValue; |
30 | import tools.refinery.store.representation.cardinality.CardinalityInterval; | ||
31 | import tools.refinery.store.representation.cardinality.CardinalityIntervals; | ||
32 | import tools.refinery.store.representation.cardinality.UpperCardinalities; | ||
15 | import tools.refinery.store.tuple.Tuple; | 33 | import tools.refinery.store.tuple.Tuple; |
16 | 34 | ||
17 | import java.util.HashMap; | 35 | import java.util.ArrayList; |
36 | import java.util.LinkedHashMap; | ||
18 | import java.util.Map; | 37 | import java.util.Map; |
19 | 38 | ||
20 | public class ModelInitializer { | 39 | public 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 | */ | ||
6 | package tools.refinery.language.semantics.model; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Singleton; | ||
10 | import org.eclipse.emf.ecore.EObject; | ||
11 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
12 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | ||
13 | |||
14 | import java.util.Optional; | ||
15 | |||
16 | @Singleton | ||
17 | public 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 | ||
8 | import org.eclipse.collections.api.factory.primitive.IntObjectMaps; | 8 | import org.eclipse.collections.api.factory.primitive.IntObjectMaps; |
9 | import tools.refinery.store.map.Cursor; | 9 | import tools.refinery.store.map.Cursor; |
10 | import tools.refinery.store.reasoning.seed.Seed; | ||
10 | import tools.refinery.store.tuple.Tuple; | 11 | import tools.refinery.store.tuple.Tuple; |
11 | import tools.refinery.store.representation.TruthValue; | 12 | import tools.refinery.store.representation.TruthValue; |
12 | 13 | ||
13 | public class DecisionTree { | 14 | public 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 { | |||
17 | dependencies { | 17 | dependencies { |
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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.IServiceResult; | ||
9 | |||
10 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import com.google.gson.JsonArray; | ||
9 | import com.google.gson.JsonObject; | ||
10 | import com.google.inject.Inject; | ||
11 | import com.google.inject.Provider; | ||
12 | import com.google.inject.Singleton; | ||
13 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
14 | import org.eclipse.xtext.util.CancelIndicator; | ||
15 | import org.eclipse.xtext.web.server.model.AbstractCachedService; | ||
16 | import org.eclipse.xtext.web.server.model.IXtextWebDocument; | ||
17 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | ||
18 | import org.eclipse.xtext.web.server.validation.ValidationService; | ||
19 | import org.jetbrains.annotations.Nullable; | ||
20 | import org.slf4j.Logger; | ||
21 | import org.slf4j.LoggerFactory; | ||
22 | import tools.refinery.language.model.problem.Problem; | ||
23 | import tools.refinery.language.semantics.model.ModelInitializer; | ||
24 | import tools.refinery.language.semantics.model.SemanticsUtils; | ||
25 | import tools.refinery.store.model.Model; | ||
26 | import tools.refinery.store.model.ModelStore; | ||
27 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; | ||
28 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
29 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | ||
30 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
31 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
32 | import tools.refinery.store.representation.TruthValue; | ||
33 | import tools.refinery.store.tuple.Tuple; | ||
34 | |||
35 | import java.util.Arrays; | ||
36 | import java.util.List; | ||
37 | import java.util.TreeMap; | ||
38 | |||
39 | @Singleton | ||
40 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import com.google.gson.JsonObject; | ||
9 | |||
10 | import java.util.List; | ||
11 | |||
12 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.message; | 6 | package tools.refinery.language.web.xtext.server.message; |
7 | 7 | ||
8 | import java.util.Objects; | 8 | import com.google.gson.annotations.SerializedName; |
9 | |||
10 | import org.eclipse.xtext.web.server.IServiceResult; | 9 | import org.eclipse.xtext.web.server.IServiceResult; |
11 | import org.eclipse.xtext.web.server.IUnwrappableServiceResult; | 10 | import org.eclipse.xtext.web.server.IUnwrappableServiceResult; |
12 | 11 | ||
13 | import com.google.gson.annotations.SerializedName; | 12 | import java.util.Objects; |
14 | 13 | ||
15 | public final class XtextWebOkResponse implements XtextWebResponse { | 14 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.message; | 6 | package tools.refinery.language.web.xtext.server.message; |
7 | 7 | ||
8 | public sealed interface XtextWebResponse permits XtextWebOkResponse,XtextWebErrorResponse,XtextWebPushMessage { | 8 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.push; | 6 | package tools.refinery.language.web.xtext.server.push; |
7 | 7 | ||
8 | import com.google.inject.Inject; | ||
8 | import org.eclipse.xtext.web.server.IServiceContext; | 9 | import org.eclipse.xtext.web.server.IServiceContext; |
9 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | 10 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; |
11 | import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; | ||
10 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | 12 | import org.eclipse.xtext.web.server.model.XtextWebDocument; |
11 | 13 | ||
12 | import com.google.inject.Singleton; | 14 | import com.google.inject.Singleton; |
13 | 15 | ||
16 | import tools.refinery.language.web.semantics.SemanticsService; | ||
14 | import tools.refinery.language.web.xtext.server.SubscribingServiceContext; | 17 | import tools.refinery.language.web.xtext.server.SubscribingServiceContext; |
15 | 18 | ||
16 | @Singleton | 19 | @Singleton |
17 | public class PushServiceDispatcher extends XtextServiceDispatcher { | 20 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.push; | 6 | package tools.refinery.language.web.xtext.server.push; |
7 | 7 | ||
8 | import java.util.ArrayList; | 8 | import com.google.common.collect.ImmutableList; |
9 | import java.util.HashMap; | ||
10 | import java.util.List; | ||
11 | import java.util.Map; | ||
12 | |||
13 | import org.eclipse.xtext.util.CancelIndicator; | 9 | import org.eclipse.xtext.util.CancelIndicator; |
14 | import org.eclipse.xtext.web.server.IServiceResult; | 10 | import org.eclipse.xtext.web.server.IServiceResult; |
15 | import org.eclipse.xtext.web.server.model.AbstractCachedService; | 11 | import org.eclipse.xtext.web.server.model.AbstractCachedService; |
@@ -17,11 +13,13 @@ import org.eclipse.xtext.web.server.model.DocumentSynchronizer; | |||
17 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | 13 | import org.eclipse.xtext.web.server.model.XtextWebDocument; |
18 | import org.slf4j.Logger; | 14 | import org.slf4j.Logger; |
19 | import org.slf4j.LoggerFactory; | 15 | import org.slf4j.LoggerFactory; |
20 | |||
21 | import com.google.common.collect.ImmutableList; | ||
22 | |||
23 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | 16 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; |
24 | 17 | ||
18 | import java.util.ArrayList; | ||
19 | import java.util.HashMap; | ||
20 | import java.util.List; | ||
21 | import java.util.Map; | ||
22 | |||
25 | public class PushWebDocument extends XtextWebDocument { | 23 | public 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; | |||
18 | import org.eclipse.xtext.web.server.validation.ValidationService; | 18 | import org.eclipse.xtext.web.server.validation.ValidationService; |
19 | 19 | ||
20 | import com.google.inject.Inject; | 20 | import com.google.inject.Inject; |
21 | import tools.refinery.language.web.semantics.SemanticsService; | ||
21 | 22 | ||
22 | public class PushWebDocumentAccess extends XtextWebDocumentAccess { | 23 | public 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; |
@@ -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" |