aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-09-12 21:59:50 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-09-12 21:59:50 +0200
commita2a4696fdbd6440269d576aeba7b25b2ea40d9bf (patch)
tree5cbdf981a51a09fbe162e7748555d213ca518ff4
parentfix: avoid GLOP error message on stderr (diff)
downloadrefinery-a2a4696fdbd6440269d576aeba7b25b2ea40d9bf.tar.gz
refinery-a2a4696fdbd6440269d576aeba7b25b2ea40d9bf.tar.zst
refinery-a2a4696fdbd6440269d576aeba7b25b2ea40d9bf.zip
feat: connect model generator to UI
-rw-r--r--subprojects/frontend/src/ModelWorkArea.tsx198
-rw-r--r--subprojects/frontend/src/WorkArea.tsx12
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts85
-rw-r--r--subprojects/frontend/src/editor/GenerateButton.tsx20
-rw-r--r--subprojects/frontend/src/editor/GeneratedModelStore.ts50
-rw-r--r--subprojects/frontend/src/graph/GraphArea.tsx12
-rw-r--r--subprojects/frontend/src/graph/GraphPane.tsx10
-rw-r--r--subprojects/frontend/src/index.tsx102
-rw-r--r--subprojects/frontend/src/table/RelationGrid.tsx109
-rw-r--r--subprojects/frontend/src/table/TableArea.tsx105
-rw-r--r--subprojects/frontend/src/table/TablePane.tsx9
-rw-r--r--subprojects/frontend/src/xtext/ModelGenerationService.ts46
-rw-r--r--subprojects/frontend/src/xtext/UpdateService.ts39
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts21
-rw-r--r--subprojects/frontend/src/xtext/xtextMessages.ts1
-rw-r--r--subprojects/frontend/src/xtext/xtextServiceResults.ts24
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java12
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java11
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java11
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java41
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java15
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java60
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java13
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java11
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java17
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java228
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java81
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java2
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java63
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java32
-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/push/PushServiceDispatcher.java39
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java20
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java4
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java2
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java6
36 files changed, 1278 insertions, 235 deletions
diff --git a/subprojects/frontend/src/ModelWorkArea.tsx b/subprojects/frontend/src/ModelWorkArea.tsx
new file mode 100644
index 00000000..3aba31e3
--- /dev/null
+++ b/subprojects/frontend/src/ModelWorkArea.tsx
@@ -0,0 +1,198 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import CloseIcon from '@mui/icons-material/Close';
8import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
9import CircularProgress from '@mui/material/CircularProgress';
10import IconButton from '@mui/material/IconButton';
11import Stack from '@mui/material/Stack';
12import Tab from '@mui/material/Tab';
13import Tabs from '@mui/material/Tabs';
14import { styled } from '@mui/material/styles';
15import { observer } from 'mobx-react-lite';
16
17import DirectionalSplitPane from './DirectionalSplitPane';
18import Loading from './Loading';
19import { useRootStore } from './RootStoreProvider';
20import type GeneratedModelStore from './editor/GeneratedModelStore';
21import GraphPane from './graph/GraphPane';
22import type GraphStore from './graph/GraphStore';
23import TablePane from './table/TablePane';
24import type ThemeStore from './theme/ThemeStore';
25
26const SplitGraphPane = observer(function SplitGraphPane({
27 graph,
28 themeStore,
29}: {
30 graph: GraphStore;
31 themeStore: ThemeStore;
32}): JSX.Element {
33 return (
34 <DirectionalSplitPane
35 primary={<GraphPane graph={graph} />}
36 secondary={<TablePane graph={graph} />}
37 primaryOnly={!themeStore.showTable}
38 secondaryOnly={!themeStore.showGraph}
39 />
40 );
41});
42
43const GenerationStatus = styled('div', {
44 name: 'ModelWorkArea-GenerationStatus',
45 shouldForwardProp: (prop) => prop !== 'error',
46})<{ error: boolean }>(({ error, theme }) => ({
47 color: error ? theme.palette.error.main : theme.palette.text.primary,
48 ...(error
49 ? {
50 fontWeight: theme.typography.fontWeightBold ?? 600,
51 }
52 : {}),
53}));
54
55const GeneratedModelPane = observer(function GeneratedModelPane({
56 generatedModel,
57 themeStore,
58}: {
59 generatedModel: GeneratedModelStore;
60 themeStore: ThemeStore;
61}): JSX.Element {
62 const { message, error, graph } = generatedModel;
63
64 if (graph !== undefined) {
65 return <SplitGraphPane graph={graph} themeStore={themeStore} />;
66 }
67
68 return (
69 <Stack
70 direction="column"
71 alignItems="center"
72 justifyContent="center"
73 height="100%"
74 width="100%"
75 overflow="hidden"
76 my={2}
77 >
78 <Stack
79 direction="column"
80 alignItems="center"
81 flexGrow={1}
82 flexShrink={1}
83 flexBasis={0}
84 sx={(theme) => ({
85 maxHeight: '6rem',
86 height: 'calc(100% - 8rem)',
87 marginBottom: theme.spacing(1),
88 padding: error ? 0 : theme.spacing(1),
89 color: theme.palette.text.secondary,
90 '.MuiCircularProgress-root, .MuiCircularProgress-svg, .MuiSvgIcon-root':
91 {
92 height: '100% !important',
93 width: '100% !important',
94 },
95 })}
96 >
97 {error ? (
98 <SentimentVeryDissatisfiedIcon
99 className="VisibilityDialog-emptyIcon"
100 fontSize="inherit"
101 />
102 ) : (
103 <CircularProgress color="inherit" />
104 )}
105 </Stack>
106 <GenerationStatus error={error}>{message}</GenerationStatus>
107 </Stack>
108 );
109});
110
111function ModelWorkArea(): JSX.Element {
112 const { editorStore, themeStore } = useRootStore();
113
114 if (editorStore === undefined) {
115 return <Loading />;
116 }
117
118 const { graph, generatedModels, selectedGeneratedModel } = editorStore;
119
120 const generatedModelNames: string[] = [];
121 const generatedModelTabs: JSX.Element[] = [];
122 generatedModels.forEach((value, key) => {
123 generatedModelNames.push(key);
124 /* eslint-disable react/no-array-index-key -- Key is a string here, not the array index. */
125 generatedModelTabs.push(
126 <Tab
127 label={value.title}
128 key={key}
129 onAuxClick={(event) => {
130 if (event.button === 1) {
131 editorStore.deleteGeneratedModel(key);
132 event.preventDefault();
133 event.stopPropagation();
134 }
135 }}
136 />,
137 );
138 /* eslint-enable react/no-array-index-key */
139 });
140 const generatedModel =
141 selectedGeneratedModel === undefined
142 ? undefined
143 : generatedModels.get(selectedGeneratedModel);
144 const selectedIndex =
145 selectedGeneratedModel === undefined
146 ? 0
147 : generatedModelNames.indexOf(selectedGeneratedModel) + 1;
148
149 return (
150 <Stack direction="column" height="100%" width="100%" overflow="hidden">
151 <Stack
152 direction="row"
153 sx={(theme) => ({
154 display: generatedModelNames.length === 0 ? 'none' : 'flex',
155 alignItems: 'center',
156 borderBottom: `1px solid ${theme.palette.outer.border}`,
157 })}
158 >
159 <Tabs
160 value={selectedIndex}
161 onChange={(_event, value) => {
162 if (value === 0) {
163 editorStore.selectGeneratedModel(undefined);
164 } else if (typeof value === 'number') {
165 editorStore.selectGeneratedModel(generatedModelNames[value - 1]);
166 }
167 }}
168 sx={{ flexGrow: 1 }}
169 >
170 <Tab label="Initial model" />
171 {generatedModelTabs}
172 </Tabs>
173 <IconButton
174 aria-label="Close generated model"
175 onClick={() =>
176 editorStore.deleteGeneratedModel(selectedGeneratedModel)
177 }
178 sx={{
179 display: selectedIndex === 0 ? 'none' : 'flex',
180 mx: 1,
181 }}
182 >
183 <CloseIcon fontSize="small" />
184 </IconButton>
185 </Stack>
186 {generatedModel === undefined ? (
187 <SplitGraphPane graph={graph} themeStore={themeStore} />
188 ) : (
189 <GeneratedModelPane
190 generatedModel={generatedModel}
191 themeStore={themeStore}
192 />
193 )}
194 </Stack>
195 );
196}
197
198export default observer(ModelWorkArea);
diff --git a/subprojects/frontend/src/WorkArea.tsx b/subprojects/frontend/src/WorkArea.tsx
index adb29a50..a1fbf7dc 100644
--- a/subprojects/frontend/src/WorkArea.tsx
+++ b/subprojects/frontend/src/WorkArea.tsx
@@ -7,10 +7,9 @@
7import { observer } from 'mobx-react-lite'; 7import { observer } from 'mobx-react-lite';
8 8
9import DirectionalSplitPane from './DirectionalSplitPane'; 9import DirectionalSplitPane from './DirectionalSplitPane';
10import ModelWorkArea from './ModelWorkArea';
10import { useRootStore } from './RootStoreProvider'; 11import { useRootStore } from './RootStoreProvider';
11import EditorPane from './editor/EditorPane'; 12import EditorPane from './editor/EditorPane';
12import GraphPane from './graph/GraphPane';
13import TablePane from './table/TablePane';
14 13
15export default observer(function WorkArea(): JSX.Element { 14export default observer(function WorkArea(): JSX.Element {
16 const { themeStore } = useRootStore(); 15 const { themeStore } = useRootStore();
@@ -18,14 +17,7 @@ export default observer(function WorkArea(): JSX.Element {
18 return ( 17 return (
19 <DirectionalSplitPane 18 <DirectionalSplitPane
20 primary={<EditorPane />} 19 primary={<EditorPane />}
21 secondary={ 20 secondary={<ModelWorkArea />}
22 <DirectionalSplitPane
23 primary={<GraphPane />}
24 secondary={<TablePane />}
25 primaryOnly={!themeStore.showTable}
26 secondaryOnly={!themeStore.showGraph}
27 />
28 }
29 primaryOnly={!themeStore.showGraph && !themeStore.showTable} 21 primaryOnly={!themeStore.showGraph && !themeStore.showTable}
30 secondaryOnly={!themeStore.showCode} 22 secondaryOnly={!themeStore.showCode}
31 /> 23 />
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index b5989ad1..f9a9a7da 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -32,6 +32,7 @@ import type XtextClient from '../xtext/XtextClient';
32import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; 32import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
33 33
34import EditorErrors from './EditorErrors'; 34import EditorErrors from './EditorErrors';
35import GeneratedModelStore from './GeneratedModelStore';
35import LintPanelStore from './LintPanelStore'; 36import LintPanelStore from './LintPanelStore';
36import SearchPanelStore from './SearchPanelStore'; 37import SearchPanelStore from './SearchPanelStore';
37import createEditorState from './createEditorState'; 38import createEditorState from './createEditorState';
@@ -69,6 +70,10 @@ export default class EditorStore {
69 70
70 graph: GraphStore; 71 graph: GraphStore;
71 72
73 generatedModels = new Map<string, GeneratedModelStore>();
74
75 selectedGeneratedModel: string | undefined;
76
72 constructor(initialValue: string, pwaStore: PWAStore) { 77 constructor(initialValue: string, pwaStore: PWAStore) {
73 this.id = nanoid(); 78 this.id = nanoid();
74 this.state = createEditorState(initialValue, this); 79 this.state = createEditorState(initialValue, this);
@@ -307,4 +312,84 @@ export default class EditorStore {
307 this.delayedErrors.dispose(); 312 this.delayedErrors.dispose();
308 this.disposed = true; 313 this.disposed = true;
309 } 314 }
315
316 startModelGeneration(): void {
317 this.client
318 ?.startModelGeneration()
319 ?.catch((error) => log.error('Could not start model generation', error));
320 }
321
322 addGeneratedModel(uuid: string): void {
323 this.generatedModels.set(uuid, new GeneratedModelStore());
324 this.selectGeneratedModel(uuid);
325 }
326
327 cancelModelGeneration(): void {
328 this.client
329 ?.cancelModelGeneration()
330 ?.catch((error) => log.error('Could not start model generation', error));
331 }
332
333 selectGeneratedModel(uuid: string | undefined): void {
334 if (uuid === undefined) {
335 this.selectedGeneratedModel = uuid;
336 return;
337 }
338 if (this.generatedModels.has(uuid)) {
339 this.selectedGeneratedModel = uuid;
340 return;
341 }
342 this.selectedGeneratedModel = undefined;
343 }
344
345 deleteGeneratedModel(uuid: string | undefined): void {
346 if (uuid === undefined) {
347 return;
348 }
349 if (this.selectedGeneratedModel === uuid) {
350 let previous: string | undefined;
351 let found: string | undefined;
352 this.generatedModels.forEach((_value, key) => {
353 if (key === uuid) {
354 found = previous;
355 }
356 previous = key;
357 });
358 this.selectGeneratedModel(found);
359 }
360 const generatedModel = this.generatedModels.get(uuid);
361 if (generatedModel !== undefined && generatedModel.running) {
362 this.cancelModelGeneration();
363 }
364 this.generatedModels.delete(uuid);
365 }
366
367 modelGenerationCancelled(): void {
368 this.generatedModels.forEach((value) =>
369 value.setError('Model generation cancelled'),
370 );
371 }
372
373 setGeneratedModelMessage(uuid: string, message: string): void {
374 this.generatedModels.get(uuid)?.setMessage(message);
375 }
376
377 setGeneratedModelError(uuid: string, message: string): void {
378 this.generatedModels.get(uuid)?.setError(message);
379 }
380
381 setGeneratedModelSemantics(
382 uuid: string,
383 semantics: SemanticsSuccessResult,
384 ): void {
385 this.generatedModels.get(uuid)?.setSemantics(semantics);
386 }
387
388 get generating(): boolean {
389 let generating = false;
390 this.generatedModels.forEach((value) => {
391 generating = generating || value.running;
392 });
393 return generating;
394 }
310} 395}
diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx
index 5bac0464..b8dcd531 100644
--- a/subprojects/frontend/src/editor/GenerateButton.tsx
+++ b/subprojects/frontend/src/editor/GenerateButton.tsx
@@ -5,6 +5,7 @@
5 */ 5 */
6 6
7import CancelIcon from '@mui/icons-material/Cancel'; 7import CancelIcon from '@mui/icons-material/Cancel';
8import CloseIcon from '@mui/icons-material/Close';
8import PlayArrowIcon from '@mui/icons-material/PlayArrow'; 9import PlayArrowIcon from '@mui/icons-material/PlayArrow';
9import { observer } from 'mobx-react-lite'; 10import { observer } from 'mobx-react-lite';
10 11
@@ -28,8 +29,10 @@ const GenerateButton = observer(function GenerateButton({
28 ); 29 );
29 } 30 }
30 31
31 const { analyzing, errorCount, warningCount, semanticsError } = 32 const {
32 editorStore.delayedErrors; 33 delayedErrors: { analyzing, errorCount, warningCount, semanticsError },
34 generating,
35 } = editorStore;
33 36
34 if (analyzing) { 37 if (analyzing) {
35 return ( 38 return (
@@ -39,6 +42,18 @@ const GenerateButton = observer(function GenerateButton({
39 ); 42 );
40 } 43 }
41 44
45 if (generating) {
46 return (
47 <AnimatedButton
48 color="inherit"
49 onClick={() => editorStore.cancelModelGeneration()}
50 startIcon={<CloseIcon />}
51 >
52 Cancel
53 </AnimatedButton>
54 );
55 }
56
42 if (semanticsError !== undefined && editorStore.opened) { 57 if (semanticsError !== undefined && editorStore.opened) {
43 return ( 58 return (
44 <AnimatedButton 59 <AnimatedButton
@@ -83,6 +98,7 @@ const GenerateButton = observer(function GenerateButton({
83 disabled={!editorStore.opened} 98 disabled={!editorStore.opened}
84 color={warningCount > 0 ? 'warning' : 'primary'} 99 color={warningCount > 0 ? 'warning' : 'primary'}
85 startIcon={<PlayArrowIcon />} 100 startIcon={<PlayArrowIcon />}
101 onClick={() => editorStore.startModelGeneration()}
86 > 102 >
87 {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} 103 {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`}
88 </AnimatedButton> 104 </AnimatedButton>
diff --git a/subprojects/frontend/src/editor/GeneratedModelStore.ts b/subprojects/frontend/src/editor/GeneratedModelStore.ts
new file mode 100644
index 00000000..d0181eed
--- /dev/null
+++ b/subprojects/frontend/src/editor/GeneratedModelStore.ts
@@ -0,0 +1,50 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { makeAutoObservable } from 'mobx';
8
9import GraphStore from '../graph/GraphStore';
10import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
11
12export default class GeneratedModelStore {
13 title: string;
14
15 message = 'Waiting for server';
16
17 error = false;
18
19 graph: GraphStore | undefined;
20
21 constructor() {
22 const time = new Date().toLocaleTimeString(undefined, { hour12: false });
23 this.title = `Generated at ${time}`;
24 makeAutoObservable(this);
25 }
26
27 get running(): boolean {
28 return !this.error && this.graph === undefined;
29 }
30
31 setMessage(message: string): void {
32 if (this.running) {
33 this.message = message;
34 }
35 }
36
37 setError(message: string): void {
38 if (this.running) {
39 this.error = true;
40 this.message = message;
41 }
42 }
43
44 setSemantics(semantics: SemanticsSuccessResult): void {
45 if (this.running) {
46 this.graph = new GraphStore();
47 this.graph.setSemantics(semantics);
48 }
49 }
50}
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx
index f8f40d22..d5801b9a 100644
--- a/subprojects/frontend/src/graph/GraphArea.tsx
+++ b/subprojects/frontend/src/graph/GraphArea.tsx
@@ -9,25 +9,17 @@ import { useTheme } from '@mui/material/styles';
9import { observer } from 'mobx-react-lite'; 9import { observer } from 'mobx-react-lite';
10import { useResizeDetector } from 'react-resize-detector'; 10import { useResizeDetector } from 'react-resize-detector';
11 11
12import Loading from '../Loading';
13import { useRootStore } from '../RootStoreProvider';
14
15import DotGraphVisualizer from './DotGraphVisualizer'; 12import DotGraphVisualizer from './DotGraphVisualizer';
13import type GraphStore from './GraphStore';
16import VisibilityPanel from './VisibilityPanel'; 14import VisibilityPanel from './VisibilityPanel';
17import ZoomCanvas from './ZoomCanvas'; 15import ZoomCanvas from './ZoomCanvas';
18 16
19function GraphArea(): JSX.Element { 17function GraphArea({ graph }: { graph: GraphStore }): JSX.Element {
20 const { editorStore } = useRootStore();
21 const { breakpoints } = useTheme(); 18 const { breakpoints } = useTheme();
22 const { ref, width, height } = useResizeDetector({ 19 const { ref, width, height } = useResizeDetector({
23 refreshMode: 'debounce', 20 refreshMode: 'debounce',
24 }); 21 });
25 22
26 if (editorStore === undefined) {
27 return <Loading />;
28 }
29
30 const { graph } = editorStore;
31 const breakpoint = breakpoints.values.sm; 23 const breakpoint = breakpoints.values.sm;
32 const dialog = 24 const dialog =
33 width === undefined || 25 width === undefined ||
diff --git a/subprojects/frontend/src/graph/GraphPane.tsx b/subprojects/frontend/src/graph/GraphPane.tsx
index c2ef8927..67dbfcbd 100644
--- a/subprojects/frontend/src/graph/GraphPane.tsx
+++ b/subprojects/frontend/src/graph/GraphPane.tsx
@@ -9,9 +9,15 @@ import { Suspense, lazy } from 'react';
9 9
10import Loading from '../Loading'; 10import Loading from '../Loading';
11 11
12import type GraphStore from './GraphStore';
13
12const GraphArea = lazy(() => import('./GraphArea')); 14const GraphArea = lazy(() => import('./GraphArea'));
13 15
14export default function GraphPane(): JSX.Element { 16export default function GraphPane({
17 graph,
18}: {
19 graph: GraphStore;
20}): JSX.Element {
15 return ( 21 return (
16 <Stack 22 <Stack
17 direction="column" 23 direction="column"
@@ -21,7 +27,7 @@ export default function GraphPane(): JSX.Element {
21 justifyContent="center" 27 justifyContent="center"
22 > 28 >
23 <Suspense fallback={<Loading />}> 29 <Suspense fallback={<Loading />}>
24 <GraphArea /> 30 <GraphArea graph={graph} />
25 </Suspense> 31 </Suspense>
26 </Stack> 32 </Stack>
27 ); 33 );
diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx
index e8a22e82..4b251a23 100644
--- a/subprojects/frontend/src/index.tsx
+++ b/subprojects/frontend/src/index.tsx
@@ -16,35 +16,101 @@ import RootStore from './RootStore';
16(window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled; 16(window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled;
17 17
18const initialValue = `% Metamodel 18const initialValue = `% Metamodel
19class Person { 19
20 contains Post[] posts opposite author 20abstract class CompositeElement {
21 Person[] friend opposite friend 21 contains Region[] regions
22}
23
24class Region {
25 contains Vertex[] vertices opposite region
26}
27
28abstract class Vertex {
29 container Region region opposite vertices
30 contains Transition[] outgoingTransition opposite source
31 Transition[] incomingTransition opposite target
22} 32}
23 33
24class Post { 34class Transition {
25 container Person author opposite posts 35 container Vertex source opposite outgoingTransition
26 Post replyTo 36 Vertex target opposite incomingTransition
27} 37}
28 38
39abstract class Pseudostate extends Vertex.
40
41abstract class RegularState extends Vertex.
42
43class Entry extends Pseudostate.
44
45class Exit extends Pseudostate.
46
47class Choice extends Pseudostate.
48
49class FinalState extends RegularState.
50
51class State extends RegularState, CompositeElement.
52
53class Statechart extends CompositeElement.
54
29% Constraints 55% Constraints
30error replyToNotFriend(Post x, Post y) <->
31 replyTo(x, y),
32 author(x, xAuthor),
33 author(y, yAuthor),
34 xAuthor != yAuthor,
35 !friend(xAuthor, yAuthor).
36 56
37error replyToCycle(Post x) <-> replyTo+(x, x). 57%% Entry
58
59pred entryInRegion(Region r, Entry e) <->
60 vertices(r, e).
61
62error noEntryInRegion(Region r) <->
63 !entryInRegion(r, _).
64
65error multipleEntryInRegion(Region r) <->
66 entryInRegion(r, e1),
67 entryInRegion(r, e2),
68 e1 != e2.
69
70error incomingToEntry(Transition t, Entry e) <->
71 target(t, e).
72
73error noOutgoingTransitionFromEntry(Entry e) <->
74 !source(_, e).
75
76error multipleTransitionFromEntry(Entry e, Transition t1, Transition t2) <->
77 outgoingTransition(e, t1),
78 outgoingTransition(e, t2),
79 t1 != t2.
80
81%% Exit
82
83error outgoingFromExit(Transition t, Exit e) <->
84 source(t, e).
85
86%% Final
87
88error outgoingFromFinal(Transition t, FinalState e) <->
89 source(t, e).
90
91%% State vs Region
92
93pred stateInRegion(Region r, State s) <->
94 vertices(r, s).
95
96error noStateInRegion(Region r) <->
97 !stateInRegion(r, _).
98
99%% Choice
100
101error choiceHasNoOutgoing(Choice c) <->
102 !source(_, c).
103
104error choiceHasNoIncoming(Choice c) <->
105 !target(_, c).
38 106
39% Instance model 107% Instance model
40friend(a, b).
41author(p1, a).
42author(p2, b).
43 108
44!author(Post::new, a). 109Statechart(sct).
45 110
46% Scope 111% Scope
47scope Post = 10..15, Person += 0. 112
113scope node = 20..30, Region = 2..*, Choice = 1..*, Statechart += 0.
48`; 114`;
49 115
50configure({ 116configure({
diff --git a/subprojects/frontend/src/table/RelationGrid.tsx b/subprojects/frontend/src/table/RelationGrid.tsx
deleted file mode 100644
index 004982c9..00000000
--- a/subprojects/frontend/src/table/RelationGrid.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import Box from '@mui/material/Box';
8import {
9 DataGrid,
10 type GridRenderCellParams,
11 type GridColDef,
12} from '@mui/x-data-grid';
13import { observer } from 'mobx-react-lite';
14import { useMemo } from 'react';
15
16import type GraphStore from '../graph/GraphStore';
17
18import TableToolbar from './TableToolbar';
19import ValueRenderer from './ValueRenderer';
20
21interface Row {
22 nodes: string[];
23 value: string;
24}
25
26function RelationGrid({ graph }: { graph: GraphStore }): JSX.Element {
27 const {
28 selectedSymbol,
29 semantics: { nodes, partialInterpretation },
30 } = graph;
31 const symbolName = selectedSymbol?.name;
32 const arity = selectedSymbol?.arity ?? 0;
33
34 const columns = useMemo<GridColDef<Row>[]>(() => {
35 const defs: GridColDef<Row>[] = [];
36 for (let i = 0; i < arity; i += 1) {
37 defs.push({
38 field: `n${i}`,
39 headerName: String(i + 1),
40 valueGetter: (row) => row.row.nodes[i] ?? '',
41 flex: 1,
42 });
43 }
44 defs.push({
45 field: 'value',
46 headerName: 'Value',
47 flex: 1,
48 renderCell: ({ value }: GridRenderCellParams<Row, string>) => (
49 <ValueRenderer value={value} />
50 ),
51 });
52 return defs;
53 }, [arity]);
54
55 const rows = useMemo<Row[]>(() => {
56 if (symbolName === undefined) {
57 return [];
58 }
59 const interpretation = partialInterpretation[symbolName] ?? [];
60 return interpretation.map((tuple) => {
61 const nodeNames: string[] = [];
62 for (let i = 0; i < arity; i += 1) {
63 const index = tuple[i];
64 if (typeof index === 'number') {
65 const node = nodes[index];
66 if (node !== undefined) {
67 nodeNames.push(node.name);
68 }
69 }
70 }
71 return {
72 nodes: nodeNames,
73 value: String(tuple[arity]),
74 };
75 });
76 }, [arity, nodes, partialInterpretation, symbolName]);
77
78 return (
79 <Box
80 width="100%"
81 height="100%"
82 p={1}
83 sx={(theme) => ({
84 '.MuiDataGrid-withBorderColor': {
85 borderColor:
86 theme.palette.mode === 'dark'
87 ? theme.palette.divider
88 : theme.palette.outer.border,
89 },
90 })}
91 >
92 <DataGrid
93 slots={{ toolbar: TableToolbar }}
94 slotProps={{
95 toolbar: {
96 graph,
97 },
98 }}
99 density="compact"
100 rowSelection={false}
101 columns={columns}
102 rows={rows}
103 getRowId={(row) => row.nodes.join(',')}
104 />
105 </Box>
106 );
107}
108
109export default observer(RelationGrid);
diff --git a/subprojects/frontend/src/table/TableArea.tsx b/subprojects/frontend/src/table/TableArea.tsx
index cf37b96a..166b8adf 100644
--- a/subprojects/frontend/src/table/TableArea.tsx
+++ b/subprojects/frontend/src/table/TableArea.tsx
@@ -4,21 +4,106 @@
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import Box from '@mui/material/Box';
8import {
9 DataGrid,
10 type GridRenderCellParams,
11 type GridColDef,
12} from '@mui/x-data-grid';
7import { observer } from 'mobx-react-lite'; 13import { observer } from 'mobx-react-lite';
14import { useMemo } from 'react';
8 15
9import Loading from '../Loading'; 16import type GraphStore from '../graph/GraphStore';
10import { useRootStore } from '../RootStoreProvider';
11 17
12import RelationGrid from './RelationGrid'; 18import TableToolbar from './TableToolbar';
19import ValueRenderer from './ValueRenderer';
13 20
14function TablePane(): JSX.Element { 21interface Row {
15 const { editorStore } = useRootStore(); 22 nodes: string[];
23 value: string;
24}
25
26function TableArea({ graph }: { graph: GraphStore }): JSX.Element {
27 const {
28 selectedSymbol,
29 semantics: { nodes, partialInterpretation },
30 } = graph;
31 const symbolName = selectedSymbol?.name;
32 const arity = selectedSymbol?.arity ?? 0;
33
34 const columns = useMemo<GridColDef<Row>[]>(() => {
35 const defs: GridColDef<Row>[] = [];
36 for (let i = 0; i < arity; i += 1) {
37 defs.push({
38 field: `n${i}`,
39 headerName: String(i + 1),
40 valueGetter: (row) => row.row.nodes[i] ?? '',
41 flex: 1,
42 });
43 }
44 defs.push({
45 field: 'value',
46 headerName: 'Value',
47 flex: 1,
48 renderCell: ({ value }: GridRenderCellParams<Row, string>) => (
49 <ValueRenderer value={value} />
50 ),
51 });
52 return defs;
53 }, [arity]);
16 54
17 if (editorStore === undefined) { 55 const rows = useMemo<Row[]>(() => {
18 return <Loading />; 56 if (symbolName === undefined) {
19 } 57 return [];
58 }
59 const interpretation = partialInterpretation[symbolName] ?? [];
60 return interpretation.map((tuple) => {
61 const nodeNames: string[] = [];
62 for (let i = 0; i < arity; i += 1) {
63 const index = tuple[i];
64 if (typeof index === 'number') {
65 const node = nodes[index];
66 if (node !== undefined) {
67 nodeNames.push(node.name);
68 }
69 }
70 }
71 return {
72 nodes: nodeNames,
73 value: String(tuple[arity]),
74 };
75 });
76 }, [arity, nodes, partialInterpretation, symbolName]);
20 77
21 return <RelationGrid graph={editorStore.graph} />; 78 return (
79 <Box
80 width="100%"
81 height="100%"
82 p={1}
83 sx={(theme) => ({
84 '.MuiDataGrid-withBorderColor': {
85 borderColor:
86 theme.palette.mode === 'dark'
87 ? theme.palette.divider
88 : theme.palette.outer.border,
89 },
90 })}
91 >
92 <DataGrid
93 slots={{ toolbar: TableToolbar }}
94 slotProps={{
95 toolbar: {
96 graph,
97 },
98 }}
99 density="compact"
100 rowSelection={false}
101 columns={columns}
102 rows={rows}
103 getRowId={(row) => row.nodes.join(',')}
104 />
105 </Box>
106 );
22} 107}
23 108
24export default observer(TablePane); 109export default observer(TableArea);
diff --git a/subprojects/frontend/src/table/TablePane.tsx b/subprojects/frontend/src/table/TablePane.tsx
index 01442c3a..8b640217 100644
--- a/subprojects/frontend/src/table/TablePane.tsx
+++ b/subprojects/frontend/src/table/TablePane.tsx
@@ -8,14 +8,19 @@ import Stack from '@mui/material/Stack';
8import { Suspense, lazy } from 'react'; 8import { Suspense, lazy } from 'react';
9 9
10import Loading from '../Loading'; 10import Loading from '../Loading';
11import type GraphStore from '../graph/GraphStore';
11 12
12const TableArea = lazy(() => import('./TableArea')); 13const TableArea = lazy(() => import('./TableArea'));
13 14
14export default function TablePane(): JSX.Element { 15export default function TablePane({
16 graph,
17}: {
18 graph: GraphStore;
19}): JSX.Element {
15 return ( 20 return (
16 <Stack direction="column" height="100%" overflow="auto" alignItems="center"> 21 <Stack direction="column" height="100%" overflow="auto" alignItems="center">
17 <Suspense fallback={<Loading />}> 22 <Suspense fallback={<Loading />}>
18 <TableArea /> 23 <TableArea graph={graph} />
19 </Suspense> 24 </Suspense>
20 </Stack> 25 </Stack>
21 ); 26 );
diff --git a/subprojects/frontend/src/xtext/ModelGenerationService.ts b/subprojects/frontend/src/xtext/ModelGenerationService.ts
new file mode 100644
index 00000000..1e9f837a
--- /dev/null
+++ b/subprojects/frontend/src/xtext/ModelGenerationService.ts
@@ -0,0 +1,46 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import type EditorStore from '../editor/EditorStore';
8
9import type UpdateService from './UpdateService';
10import { ModelGenerationResult } from './xtextServiceResults';
11
12export default class ModelGenerationService {
13 constructor(
14 private readonly store: EditorStore,
15 private readonly updateService: UpdateService,
16 ) {}
17
18 onPush(push: unknown): void {
19 const result = ModelGenerationResult.parse(push);
20 if ('status' in result) {
21 this.store.setGeneratedModelMessage(result.uuid, result.status);
22 } else if ('error' in result) {
23 this.store.setGeneratedModelError(result.uuid, result.error);
24 } else {
25 this.store.setGeneratedModelSemantics(result.uuid, result);
26 }
27 }
28
29 onDisconnect(): void {
30 this.store.modelGenerationCancelled();
31 }
32
33 async start(): Promise<void> {
34 const result = await this.updateService.startModelGeneration();
35 if (!result.cancelled) {
36 this.store.addGeneratedModel(result.data.uuid);
37 }
38 }
39
40 async cancel(): Promise<void> {
41 const result = await this.updateService.cancelModelGeneration();
42 if (!result.cancelled) {
43 this.store.modelGenerationCancelled();
44 }
45 }
46}
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts
index 1ac722e1..d1246d5e 100644
--- a/subprojects/frontend/src/xtext/UpdateService.ts
+++ b/subprojects/frontend/src/xtext/UpdateService.ts
@@ -22,6 +22,7 @@ import {
22 FormattingResult, 22 FormattingResult,
23 isConflictResult, 23 isConflictResult,
24 OccurrencesResult, 24 OccurrencesResult,
25 ModelGenerationStartedResult,
25} from './xtextServiceResults'; 26} from './xtextServiceResults';
26 27
27const UPDATE_TIMEOUT_MS = 500; 28const UPDATE_TIMEOUT_MS = 500;
@@ -341,4 +342,42 @@ export default class UpdateService {
341 } 342 }
342 return { cancelled: false, data: parsedOccurrencesResult }; 343 return { cancelled: false, data: parsedOccurrencesResult };
343 } 344 }
345
346 async startModelGeneration(): Promise<
347 CancellableResult<ModelGenerationStartedResult>
348 > {
349 try {
350 await this.updateOrThrow();
351 } catch (error) {
352 if (error instanceof CancelledError || error instanceof TimeoutError) {
353 return { cancelled: true };
354 }
355 throw error;
356 }
357 log.debug('Starting model generation');
358 const data = await this.webSocketClient.send({
359 resource: this.resourceName,
360 serviceType: 'modelGeneration',
361 requiredStateId: this.xtextStateId,
362 start: true,
363 });
364 if (isConflictResult(data)) {
365 return { cancelled: true };
366 }
367 const parsedResult = ModelGenerationStartedResult.parse(data);
368 return { cancelled: false, data: parsedResult };
369 }
370
371 async cancelModelGeneration(): Promise<CancellableResult<unknown>> {
372 log.debug('Cancelling model generation');
373 const data = await this.webSocketClient.send({
374 resource: this.resourceName,
375 serviceType: 'modelGeneration',
376 cancel: true,
377 });
378 if (isConflictResult(data)) {
379 return { cancelled: true };
380 }
381 return { cancelled: false, data };
382 }
344} 383}
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts
index 87778084..77980d35 100644
--- a/subprojects/frontend/src/xtext/XtextClient.ts
+++ b/subprojects/frontend/src/xtext/XtextClient.ts
@@ -16,6 +16,7 @@ import getLogger from '../utils/getLogger';
16 16
17import ContentAssistService from './ContentAssistService'; 17import ContentAssistService from './ContentAssistService';
18import HighlightingService from './HighlightingService'; 18import HighlightingService from './HighlightingService';
19import ModelGenerationService from './ModelGenerationService';
19import OccurrencesService from './OccurrencesService'; 20import OccurrencesService from './OccurrencesService';
20import SemanticsService from './SemanticsService'; 21import SemanticsService from './SemanticsService';
21import UpdateService from './UpdateService'; 22import UpdateService from './UpdateService';
@@ -40,6 +41,8 @@ export default class XtextClient {
40 41
41 private readonly semanticsService: SemanticsService; 42 private readonly semanticsService: SemanticsService;
42 43
44 private readonly modelGenerationService: ModelGenerationService;
45
43 constructor( 46 constructor(
44 private readonly store: EditorStore, 47 private readonly store: EditorStore,
45 private readonly pwaStore: PWAStore, 48 private readonly pwaStore: PWAStore,
@@ -58,6 +61,10 @@ export default class XtextClient {
58 this.validationService = new ValidationService(store, this.updateService); 61 this.validationService = new ValidationService(store, this.updateService);
59 this.occurrencesService = new OccurrencesService(store, this.updateService); 62 this.occurrencesService = new OccurrencesService(store, this.updateService);
60 this.semanticsService = new SemanticsService(store, this.validationService); 63 this.semanticsService = new SemanticsService(store, this.validationService);
64 this.modelGenerationService = new ModelGenerationService(
65 store,
66 this.updateService,
67 );
61 } 68 }
62 69
63 start(): void { 70 start(): void {
@@ -75,6 +82,7 @@ export default class XtextClient {
75 this.highlightingService.onDisconnect(); 82 this.highlightingService.onDisconnect();
76 this.validationService.onDisconnect(); 83 this.validationService.onDisconnect();
77 this.occurrencesService.onDisconnect(); 84 this.occurrencesService.onDisconnect();
85 this.modelGenerationService.onDisconnect();
78 } 86 }
79 87
80 onTransaction(transaction: Transaction): void { 88 onTransaction(transaction: Transaction): void {
@@ -101,7 +109,7 @@ export default class XtextClient {
101 ); 109 );
102 return; 110 return;
103 } 111 }
104 if (stateId !== xtextStateId) { 112 if (stateId !== xtextStateId && service !== 'modelGeneration') {
105 log.error( 113 log.error(
106 'Unexpected xtext state id: expected:', 114 'Unexpected xtext state id: expected:',
107 xtextStateId, 115 xtextStateId,
@@ -122,6 +130,9 @@ export default class XtextClient {
122 case 'semantics': 130 case 'semantics':
123 this.semanticsService.onPush(push); 131 this.semanticsService.onPush(push);
124 return; 132 return;
133 case 'modelGeneration':
134 this.modelGenerationService.onPush(push);
135 return;
125 default: 136 default:
126 throw new Error('Unknown service'); 137 throw new Error('Unknown service');
127 } 138 }
@@ -131,6 +142,14 @@ export default class XtextClient {
131 return this.contentAssistService.contentAssist(context); 142 return this.contentAssistService.contentAssist(context);
132 } 143 }
133 144
145 startModelGeneration(): Promise<void> {
146 return this.modelGenerationService.start();
147 }
148
149 cancelModelGeneration(): Promise<void> {
150 return this.modelGenerationService.cancel();
151 }
152
134 formatText(): void { 153 formatText(): void {
135 this.updateService.formatText().catch((e) => { 154 this.updateService.formatText().catch((e) => {
136 log.error('Error while formatting text', e); 155 log.error('Error while formatting text', e);
diff --git a/subprojects/frontend/src/xtext/xtextMessages.ts b/subprojects/frontend/src/xtext/xtextMessages.ts
index 971720e1..15831c5a 100644
--- a/subprojects/frontend/src/xtext/xtextMessages.ts
+++ b/subprojects/frontend/src/xtext/xtextMessages.ts
@@ -38,6 +38,7 @@ export const XtextWebPushService = z.enum([
38 'highlight', 38 'highlight',
39 'validate', 39 'validate',
40 'semantics', 40 'semantics',
41 'modelGeneration',
41]); 42]);
42 43
43export type XtextWebPushService = z.infer<typeof XtextWebPushService>; 44export type XtextWebPushService = z.infer<typeof XtextWebPushService>;
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts
index caf2cf0b..e473bd48 100644
--- a/subprojects/frontend/src/xtext/xtextServiceResults.ts
+++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts
@@ -126,6 +126,14 @@ export const FormattingResult = DocumentStateResult.extend({
126 126
127export type FormattingResult = z.infer<typeof FormattingResult>; 127export type FormattingResult = z.infer<typeof FormattingResult>;
128 128
129export const ModelGenerationStartedResult = z.object({
130 uuid: z.string().nonempty(),
131});
132
133export type ModelGenerationStartedResult = z.infer<
134 typeof ModelGenerationStartedResult
135>;
136
129export const NodeMetadata = z.object({ 137export const NodeMetadata = z.object({
130 name: z.string(), 138 name: z.string(),
131 simpleName: z.string(), 139 simpleName: z.string(),
@@ -171,3 +179,19 @@ export const SemanticsResult = z.union([
171]); 179]);
172 180
173export type SemanticsResult = z.infer<typeof SemanticsResult>; 181export type SemanticsResult = z.infer<typeof SemanticsResult>;
182
183export const ModelGenerationResult = z.union([
184 z.object({
185 uuid: z.string().nonempty(),
186 status: z.string(),
187 }),
188 z.object({
189 uuid: z.string().nonempty(),
190 error: z.string(),
191 }),
192 SemanticsSuccessResult.extend({
193 uuid: z.string().nonempty(),
194 }),
195]);
196
197export type ModelGenerationResult = z.infer<typeof ModelGenerationResult>;
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java
index 0c18b1b3..d6115c5c 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java
@@ -47,12 +47,22 @@ public class MetadataCreator {
47 } 47 }
48 48
49 public List<NodeMetadata> getNodesMetadata() { 49 public List<NodeMetadata> getNodesMetadata() {
50 var nodes = new NodeMetadata[initializer.getNodeCount()]; 50 return getNodesMetadata(initializer.getNodeCount());
51 }
52
53 public List<NodeMetadata> getNodesMetadata(int nodeCount) {
54 var nodes = new NodeMetadata[Math.max(initializer.getNodeCount(), nodeCount)];
51 for (var entry : initializer.getNodeTrace().keyValuesView()) { 55 for (var entry : initializer.getNodeTrace().keyValuesView()) {
52 var node = entry.getOne(); 56 var node = entry.getOne();
53 var id = entry.getTwo(); 57 var id = entry.getTwo();
54 nodes[id] = getNodeMetadata(node); 58 nodes[id] = getNodeMetadata(node);
55 } 59 }
60 for (int i = 0; i < nodes.length; i++) {
61 if (nodes[i] == null) {
62 var nodeName = "#" + i;
63 nodes[i] = new NodeMetadata(nodeName, nodeName, NodeKind.IMPLICIT);
64 }
65 }
56 return List.of(nodes); 66 return List.of(nodes);
57 } 67 }
58 68
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java
new file mode 100644
index 00000000..fc06fd2e
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.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.generator;
7
8import org.eclipse.xtext.web.server.IServiceResult;
9
10public record ModelGenerationCancelledResult() implements IServiceResult {
11}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java
new file mode 100644
index 00000000..bedaeb35
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.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.generator;
7
8import java.util.UUID;
9
10public record ModelGenerationErrorResult(UUID uuid, String error) implements ModelGenerationResult {
11}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java
new file mode 100644
index 00000000..b0a1912c
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java
@@ -0,0 +1,41 @@
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.generator;
7
8import org.eclipse.xtext.util.CancelIndicator;
9
10public class ModelGenerationManager {
11 private final Object lockObject = new Object();
12 private ModelGenerationWorker worker;
13 private boolean disposed;
14
15 boolean setActiveModelGenerationWorker(ModelGenerationWorker worker, CancelIndicator cancelIndicator) {
16 synchronized (lockObject) {
17 cancel();
18 if (disposed || cancelIndicator.isCanceled()) {
19 return true;
20 }
21 this.worker = worker;
22 }
23 return false;
24 }
25
26 public void cancel() {
27 synchronized (lockObject) {
28 if (worker != null) {
29 worker.cancel();
30 worker = null;
31 }
32 }
33 }
34
35 public void dispose() {
36 synchronized (lockObject) {
37 disposed = true;
38 cancel();
39 }
40 }
41}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java
new file mode 100644
index 00000000..cf06f447
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java
@@ -0,0 +1,15 @@
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.generator;
7
8import org.eclipse.xtext.web.server.IServiceResult;
9
10import java.util.UUID;
11
12public sealed interface ModelGenerationResult extends IServiceResult permits ModelGenerationSuccessResult,
13 ModelGenerationErrorResult, ModelGenerationStatusResult {
14 UUID uuid();
15}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java
new file mode 100644
index 00000000..5a60007f
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java
@@ -0,0 +1,60 @@
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.generator;
7
8import com.google.inject.Inject;
9import com.google.inject.Provider;
10import com.google.inject.Singleton;
11import org.eclipse.xtext.service.OperationCanceledManager;
12import org.eclipse.xtext.util.CancelIndicator;
13import org.eclipse.xtext.util.concurrent.CancelableUnitOfWork;
14import org.eclipse.xtext.web.server.model.IXtextWebDocument;
15import tools.refinery.language.web.semantics.SemanticsService;
16import tools.refinery.language.web.xtext.server.push.PushWebDocument;
17import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess;
18
19@Singleton
20public class ModelGenerationService {
21 public static final String SERVICE_NAME = "modelGeneration";
22 public static final String MODEL_GENERATION_EXECUTOR = "modelGeneration";
23 public static final String MODEL_GENERATION_TIMEOUT_EXECUTOR = "modelGenerationTimeout";
24
25 @Inject
26 private OperationCanceledManager operationCanceledManager;
27
28 @Inject
29 private Provider<ModelGenerationWorker> workerProvider;
30
31 private final long timeoutSec;
32
33 public ModelGenerationService() {
34 timeoutSec = SemanticsService.getTimeout("REFINERY_MODEL_GENERATION_TIMEOUT_SEC").orElse(600L);
35 }
36
37 public ModelGenerationStartedResult generateModel(PushWebDocumentAccess document){
38 return document.modify(new CancelableUnitOfWork<>() {
39 @Override
40 public ModelGenerationStartedResult exec(IXtextWebDocument state, CancelIndicator cancelIndicator) {
41 var pushState = (PushWebDocument) state;
42 var worker = workerProvider.get();
43 worker.setState(pushState, timeoutSec);
44 var manager = pushState.getModelGenerationManager();
45 worker.start();
46 boolean canceled = manager.setActiveModelGenerationWorker(worker, cancelIndicator);
47 if (canceled) {
48 worker.cancel();
49 operationCanceledManager.throwOperationCanceledException();
50 }
51 return new ModelGenerationStartedResult(worker.getUuid());
52 }
53 });
54 }
55
56 public ModelGenerationCancelledResult cancelModelGeneration(PushWebDocumentAccess document) {
57 document.cancelModelGeneration();
58 return new ModelGenerationCancelledResult();
59 }
60}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java
new file mode 100644
index 00000000..8c0e73c7
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.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.generator;
7
8import org.eclipse.xtext.web.server.IServiceResult;
9
10import java.util.UUID;
11
12public record ModelGenerationStartedResult(UUID uuid) implements IServiceResult {
13}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java
new file mode 100644
index 00000000..a6589870
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.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.generator;
7
8import java.util.UUID;
9
10public record ModelGenerationStatusResult(UUID uuid, String status) implements ModelGenerationResult {
11}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java
new file mode 100644
index 00000000..21be4e08
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java
@@ -0,0 +1,17 @@
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.generator;
7
8import com.google.gson.JsonObject;
9import tools.refinery.language.semantics.metadata.NodeMetadata;
10import tools.refinery.language.semantics.metadata.RelationMetadata;
11
12import java.util.List;
13import java.util.UUID;
14
15public record ModelGenerationSuccessResult(UUID uuid, List<NodeMetadata> nodes, List<RelationMetadata> relations,
16 JsonObject partialInterpretation) implements ModelGenerationResult {
17}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java
new file mode 100644
index 00000000..1f430da6
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java
@@ -0,0 +1,228 @@
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.generator;
7
8import com.google.inject.Inject;
9import com.google.inject.Provider;
10import org.eclipse.emf.common.util.URI;
11import org.eclipse.xtext.diagnostics.Severity;
12import org.eclipse.xtext.resource.IResourceFactory;
13import org.eclipse.xtext.resource.XtextResourceSet;
14import org.eclipse.xtext.service.OperationCanceledManager;
15import org.eclipse.xtext.util.LazyStringInputStream;
16import org.eclipse.xtext.validation.CheckMode;
17import org.eclipse.xtext.validation.IResourceValidator;
18import org.slf4j.Logger;
19import org.slf4j.LoggerFactory;
20import tools.refinery.language.model.problem.Problem;
21import tools.refinery.language.semantics.metadata.MetadataCreator;
22import tools.refinery.language.semantics.model.ModelInitializer;
23import tools.refinery.language.web.semantics.PartialInterpretation2Json;
24import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider;
25import tools.refinery.language.web.xtext.server.push.PushWebDocument;
26import tools.refinery.store.dse.propagation.PropagationAdapter;
27import tools.refinery.store.dse.strategy.BestFirstStoreManager;
28import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter;
29import tools.refinery.store.model.ModelStore;
30import tools.refinery.store.query.viatra.ViatraModelQueryAdapter;
31import tools.refinery.store.reasoning.ReasoningAdapter;
32import tools.refinery.store.reasoning.ReasoningStoreAdapter;
33import tools.refinery.store.reasoning.literal.Concreteness;
34import tools.refinery.store.statecoding.StateCoderAdapter;
35import tools.refinery.store.util.CancellationToken;
36
37import java.io.IOException;
38import java.util.Map;
39import java.util.UUID;
40import java.util.concurrent.*;
41
42public class ModelGenerationWorker implements Runnable {
43 private static final Logger LOG = LoggerFactory.getLogger(ModelGenerationWorker.class);
44
45 private final UUID uuid = UUID.randomUUID();
46
47 private PushWebDocument state;
48
49 private String text;
50
51 private volatile boolean timedOut;
52
53 private volatile boolean cancelled;
54
55 @Inject
56 private OperationCanceledManager operationCanceledManager;
57
58 @Inject
59 private Provider<XtextResourceSet> resourceSetProvider;
60
61 @Inject
62 private IResourceFactory resourceFactory;
63
64 @Inject
65 private IResourceValidator resourceValidator;
66
67 @Inject
68 private ModelInitializer initializer;
69
70 @Inject
71 private MetadataCreator metadataCreator;
72
73 @Inject
74 private PartialInterpretation2Json partialInterpretation2Json;
75
76 private final Object lockObject = new Object();
77
78 private ExecutorService executorService;
79
80 private ScheduledExecutorService scheduledExecutorService;
81
82 private long timeoutSec;
83
84 private Future<?> future;
85
86 private ScheduledFuture<?> timeoutFuture;
87
88 private final CancellationToken cancellationToken = () -> {
89 if (cancelled || Thread.interrupted()) {
90 operationCanceledManager.throwOperationCanceledException();
91 }
92 };
93
94 @Inject
95 public void setExecutorServiceProvider(ThreadPoolExecutorServiceProvider provider) {
96 executorService = provider.get(ModelGenerationService.MODEL_GENERATION_EXECUTOR);
97 scheduledExecutorService = provider.getScheduled(ModelGenerationService.MODEL_GENERATION_TIMEOUT_EXECUTOR);
98 }
99
100 public void setState(PushWebDocument state, long timeoutSec) {
101 this.state = state;
102 this.timeoutSec = timeoutSec;
103 text = state.getText();
104 }
105
106 public UUID getUuid() {
107 return uuid;
108 }
109
110 public void start() {
111 synchronized (lockObject) {
112 LOG.debug("Enqueueing model generation: {}", uuid);
113 future = executorService.submit(this);
114 }
115 }
116
117 public void startTimeout() {
118 synchronized (lockObject) {
119 LOG.debug("Starting model generation: {}", uuid);
120 cancellationToken.checkCancelled();
121 timeoutFuture = scheduledExecutorService.schedule(() -> cancel(true), timeoutSec, TimeUnit.SECONDS);
122 }
123 }
124
125 // We catch {@code Throwable} to handle {@code OperationCancelledError}, but we rethrow fatal JVM errors.
126 @SuppressWarnings("squid:S1181")
127 @Override
128 public void run() {
129 startTimeout();
130 notifyResult(new ModelGenerationStatusResult(uuid, "Initializing model generator"));
131 ModelGenerationResult result;
132 try {
133 result = doRun();
134 } catch (Throwable e) {
135 if (operationCanceledManager.isOperationCanceledException(e)) {
136 var message = timedOut ? "Model generation timed out" : "Model generation cancelled";
137 LOG.debug("{}: {}", message, uuid);
138 notifyResult(new ModelGenerationErrorResult(uuid, message));
139 } else if (e instanceof Error error) {
140 // Make sure we don't try to recover from any fatal JVM errors.
141 throw error;
142 } else {
143 LOG.debug("Model generation error", e);
144 notifyResult(new ModelGenerationErrorResult(uuid, e.toString()));
145 }
146 return;
147 }
148 notifyResult(result);
149 }
150
151 private void notifyResult(ModelGenerationResult result) {
152 state.notifyPrecomputationListeners(ModelGenerationService.SERVICE_NAME, result);
153 }
154
155 public ModelGenerationResult doRun() throws IOException {
156 cancellationToken.checkCancelled();
157 var resourceSet = resourceSetProvider.get();
158 var uri = URI.createURI("__synthetic_" + uuid + ".problem");
159 var resource = resourceFactory.createResource(uri);
160 resourceSet.getResources().add(resource);
161 var inputStream = new LazyStringInputStream(text);
162 resource.load(inputStream, Map.of());
163 cancellationToken.checkCancelled();
164 var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> cancelled || Thread.interrupted());
165 cancellationToken.checkCancelled();
166 for (var issue : issues) {
167 if (issue.getSeverity() == Severity.ERROR) {
168 return new ModelGenerationErrorResult(uuid, "Validation error: " + issue.getMessage());
169 }
170 }
171 if (resource.getContents().isEmpty() || !(resource.getContents().get(0) instanceof Problem problem)) {
172 return new ModelGenerationErrorResult(uuid, "Model generation problem not found");
173 }
174 cancellationToken.checkCancelled();
175 var storeBuilder = ModelStore.builder()
176 .cancellationToken(cancellationToken)
177 .with(ViatraModelQueryAdapter.builder())
178 .with(PropagationAdapter.builder())
179 .with(StateCoderAdapter.builder())
180 .with(DesignSpaceExplorationAdapter.builder())
181 .with(ReasoningAdapter.builder());
182 var modelSeed = initializer.createModel(problem, storeBuilder);
183 var store = storeBuilder.build();
184 cancellationToken.checkCancelled();
185 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed);
186 var initialVersion = model.commit();
187 cancellationToken.checkCancelled();
188 notifyResult(new ModelGenerationStatusResult(uuid, "Generating model"));
189 var bestFirst = new BestFirstStoreManager(store, 1);
190 bestFirst.startExploration(initialVersion);
191 cancellationToken.checkCancelled();
192 var solutionStore = bestFirst.getSolutionStore();
193 if (solutionStore.getSolutions().isEmpty()) {
194 return new ModelGenerationErrorResult(uuid, "Problem is unsatisfiable");
195 }
196 notifyResult(new ModelGenerationStatusResult(uuid, "Saving generated model"));
197 model.restore(solutionStore.getSolutions().get(0).version());
198 cancellationToken.checkCancelled();
199 metadataCreator.setInitializer(initializer);
200 var nodesMetadata = metadataCreator.getNodesMetadata(model.getAdapter(ReasoningAdapter.class).getNodeCount());
201 cancellationToken.checkCancelled();
202 var relationsMetadata = metadataCreator.getRelationsMetadata();
203 cancellationToken.checkCancelled();
204 var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model,
205 Concreteness.CANDIDATE, cancellationToken);
206 return new ModelGenerationSuccessResult(uuid, nodesMetadata, relationsMetadata, partialInterpretation);
207 }
208
209 public void cancel() {
210 cancel(false);
211 }
212
213 public void cancel(boolean timedOut) {
214 synchronized (lockObject) {
215 LOG.trace("Cancelling model generation: {}", uuid);
216 this.timedOut = timedOut;
217 cancelled = true;
218 if (future != null) {
219 future.cancel(true);
220 future = null;
221 }
222 if (timeoutFuture != null) {
223 timeoutFuture.cancel(true);
224 timeoutFuture = null;
225 }
226 }
227 }
228}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java
new file mode 100644
index 00000000..5d5da8fe
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java
@@ -0,0 +1,81 @@
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.Singleton;
12import tools.refinery.language.semantics.model.ModelInitializer;
13import tools.refinery.language.semantics.model.SemanticsUtils;
14import tools.refinery.store.map.Cursor;
15import tools.refinery.store.model.Model;
16import tools.refinery.store.reasoning.ReasoningAdapter;
17import tools.refinery.store.reasoning.literal.Concreteness;
18import tools.refinery.store.reasoning.representation.PartialRelation;
19import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator;
20import tools.refinery.store.tuple.Tuple;
21import tools.refinery.store.util.CancellationToken;
22
23import java.util.TreeMap;
24
25@Singleton
26public class PartialInterpretation2Json {
27 @Inject
28 private SemanticsUtils semanticsUtils;
29
30 public JsonObject getPartialInterpretation(ModelInitializer initializer, Model model, Concreteness concreteness,
31 CancellationToken cancellationToken) {
32 var adapter = model.getAdapter(ReasoningAdapter.class);
33 var json = new JsonObject();
34 for (var entry : initializer.getRelationTrace().entrySet()) {
35 var relation = entry.getKey();
36 var partialSymbol = entry.getValue();
37 var tuples = getTuplesJson(adapter, concreteness, partialSymbol);
38 var name = semanticsUtils.getName(relation).orElse(partialSymbol.name());
39 json.add(name, tuples);
40 cancellationToken.checkCancelled();
41 }
42 json.add("builtin::count", getCountJson(model));
43 return json;
44 }
45
46 private static JsonArray getTuplesJson(ReasoningAdapter adapter, Concreteness concreteness,
47 PartialRelation partialSymbol) {
48 var interpretation = adapter.getPartialInterpretation(concreteness, partialSymbol);
49 var cursor = interpretation.getAll();
50 return getTuplesJson(cursor);
51 }
52
53 private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) {
54 var map = new TreeMap<Tuple, Object>();
55 while (cursor.move()) {
56 map.put(cursor.getKey(), cursor.getValue());
57 }
58 var tuples = new JsonArray();
59 for (var entry : map.entrySet()) {
60 tuples.add(toArray(entry.getKey(), entry.getValue()));
61 }
62 return tuples;
63 }
64
65 private static JsonArray toArray(Tuple tuple, Object value) {
66 int arity = tuple.getSize();
67 var json = new JsonArray(arity + 1);
68 for (int i = 0; i < arity; i++) {
69 json.add(tuple.get(i));
70 }
71 json.add(value.toString());
72 return json;
73 }
74
75 private static JsonArray getCountJson(Model model) {
76 var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE);
77 var cursor = interpretation.getAll();
78 return getTuplesJson(cursor);
79
80 }
81}
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
index 26924f0a..331ef84b 100644
--- 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
@@ -54,7 +54,7 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> {
54 warmupTimeoutMs = getTimeout("REFINERY_SEMANTICS_WARMUP_TIMEOUT_MS").orElse(timeoutMs * 2); 54 warmupTimeoutMs = getTimeout("REFINERY_SEMANTICS_WARMUP_TIMEOUT_MS").orElse(timeoutMs * 2);
55 } 55 }
56 56
57 private static Optional<Long> getTimeout(String name) { 57 public static Optional<Long> getTimeout(String name) {
58 return Optional.ofNullable(System.getenv(name)).map(Long::parseUnsignedLong); 58 return Optional.ofNullable(System.getenv(name)).map(Long::parseUnsignedLong);
59 } 59 }
60 60
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
index 33b1c4fb..512c2778 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
@@ -5,8 +5,6 @@
5 */ 5 */
6package tools.refinery.language.web.semantics; 6package tools.refinery.language.web.semantics;
7 7
8import com.google.gson.JsonArray;
9import com.google.gson.JsonObject;
10import com.google.inject.Inject; 8import com.google.inject.Inject;
11import org.eclipse.emf.common.util.Diagnostic; 9import org.eclipse.emf.common.util.Diagnostic;
12import org.eclipse.emf.ecore.EObject; 10import org.eclipse.emf.ecore.EObject;
@@ -20,31 +18,24 @@ import org.eclipse.xtext.web.server.validation.ValidationResult;
20import tools.refinery.language.model.problem.Problem; 18import tools.refinery.language.model.problem.Problem;
21import tools.refinery.language.semantics.metadata.MetadataCreator; 19import tools.refinery.language.semantics.metadata.MetadataCreator;
22import tools.refinery.language.semantics.model.ModelInitializer; 20import tools.refinery.language.semantics.model.ModelInitializer;
23import tools.refinery.language.semantics.model.SemanticsUtils;
24import tools.refinery.language.semantics.model.TracedException; 21import tools.refinery.language.semantics.model.TracedException;
25import tools.refinery.store.dse.propagation.PropagationAdapter; 22import tools.refinery.store.dse.propagation.PropagationAdapter;
26import tools.refinery.store.map.Cursor;
27import tools.refinery.store.model.Model;
28import tools.refinery.store.model.ModelStore; 23import tools.refinery.store.model.ModelStore;
29import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; 24import tools.refinery.store.query.viatra.ViatraModelQueryAdapter;
30import tools.refinery.store.reasoning.ReasoningAdapter; 25import tools.refinery.store.reasoning.ReasoningAdapter;
31import tools.refinery.store.reasoning.ReasoningStoreAdapter; 26import tools.refinery.store.reasoning.ReasoningStoreAdapter;
32import tools.refinery.store.reasoning.literal.Concreteness; 27import tools.refinery.store.reasoning.literal.Concreteness;
33import tools.refinery.store.reasoning.representation.PartialRelation;
34import tools.refinery.store.reasoning.translator.TranslationException; 28import tools.refinery.store.reasoning.translator.TranslationException;
35import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator;
36import tools.refinery.store.tuple.Tuple;
37import tools.refinery.store.util.CancellationToken; 29import tools.refinery.store.util.CancellationToken;
38 30
39import java.util.ArrayList; 31import java.util.ArrayList;
40import java.util.TreeMap;
41import java.util.concurrent.Callable; 32import java.util.concurrent.Callable;
42 33
43class SemanticsWorker implements Callable<SemanticsResult> { 34class SemanticsWorker implements Callable<SemanticsResult> {
44 private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError"; 35 private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError";
45 36
46 @Inject 37 @Inject
47 private SemanticsUtils semanticsUtils; 38 private PartialInterpretation2Json partialInterpretation2Json;
48 39
49 @Inject 40 @Inject
50 private OperationCanceledManager operationCanceledManager; 41 private OperationCanceledManager operationCanceledManager;
@@ -93,7 +84,8 @@ class SemanticsWorker implements Callable<SemanticsResult> {
93 cancellationToken.checkCancelled(); 84 cancellationToken.checkCancelled();
94 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); 85 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed);
95 cancellationToken.checkCancelled(); 86 cancellationToken.checkCancelled();
96 var partialInterpretation = getPartialInterpretation(initializer, model); 87 var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model,
88 Concreteness.PARTIAL, cancellationToken);
97 89
98 return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation); 90 return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation);
99 } catch (TracedException e) { 91 } catch (TracedException e) {
@@ -104,55 +96,6 @@ class SemanticsWorker implements Callable<SemanticsResult> {
104 } 96 }
105 } 97 }
106 98
107 private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model) {
108 var adapter = model.getAdapter(ReasoningAdapter.class);
109 var json = new JsonObject();
110 for (var entry : initializer.getRelationTrace().entrySet()) {
111 var relation = entry.getKey();
112 var partialSymbol = entry.getValue();
113 var tuples = getTuplesJson(adapter, partialSymbol);
114 var name = semanticsUtils.getName(relation).orElse(partialSymbol.name());
115 json.add(name, tuples);
116 cancellationToken.checkCancelled();
117 }
118 json.add("builtin::count", getCountJson(model));
119 return json;
120 }
121
122 private static JsonArray getTuplesJson(ReasoningAdapter adapter, PartialRelation partialSymbol) {
123 var interpretation = adapter.getPartialInterpretation(Concreteness.PARTIAL, partialSymbol);
124 var cursor = interpretation.getAll();
125 return getTuplesJson(cursor);
126 }
127
128 private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) {
129 var map = new TreeMap<Tuple, Object>();
130 while (cursor.move()) {
131 map.put(cursor.getKey(), cursor.getValue());
132 }
133 var tuples = new JsonArray();
134 for (var entry : map.entrySet()) {
135 tuples.add(toArray(entry.getKey(), entry.getValue()));
136 }
137 return tuples;
138 }
139
140 private static JsonArray toArray(Tuple tuple, Object value) {
141 int arity = tuple.getSize();
142 var json = new JsonArray(arity + 1);
143 for (int i = 0; i < arity; i++) {
144 json.add(tuple.get(i));
145 }
146 json.add(value.toString());
147 return json;
148 }
149
150 private static JsonArray getCountJson(Model model) {
151 var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE);
152 var cursor = interpretation.getAll();
153 return getTuplesJson(cursor);
154 }
155
156 private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) { 99 private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) {
157 if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) { 100 if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) {
158 return new SemanticsInternalErrorResult(message); 101 return new SemanticsInternalErrorResult(message);
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java
index ba26ff58..625909b9 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java
@@ -13,9 +13,13 @@ import tools.refinery.language.web.semantics.SemanticsService;
13 13
14import java.lang.invoke.MethodHandle; 14import java.lang.invoke.MethodHandle;
15import java.lang.invoke.MethodHandles; 15import java.lang.invoke.MethodHandles;
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.Map;
16import java.util.Optional; 19import java.util.Optional;
17import java.util.concurrent.ExecutorService; 20import java.util.concurrent.ExecutorService;
18import java.util.concurrent.Executors; 21import java.util.concurrent.Executors;
22import java.util.concurrent.ScheduledExecutorService;
19import java.util.concurrent.ThreadFactory; 23import java.util.concurrent.ThreadFactory;
20import java.util.concurrent.atomic.AtomicInteger; 24import java.util.concurrent.atomic.AtomicInteger;
21 25
@@ -24,6 +28,8 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider {
24 private static final String DOCUMENT_LOCK_EXECUTOR; 28 private static final String DOCUMENT_LOCK_EXECUTOR;
25 private static final AtomicInteger POOL_ID = new AtomicInteger(1); 29 private static final AtomicInteger POOL_ID = new AtomicInteger(1);
26 30
31 private final Map<String, ScheduledExecutorService> scheduledInstanceCache =
32 Collections.synchronizedMap(new HashMap<>());
27 private final int executorThreadCount; 33 private final int executorThreadCount;
28 private final int lockExecutorThreadCount; 34 private final int lockExecutorThreadCount;
29 private final int semanticsExecutorThreadCount; 35 private final int semanticsExecutorThreadCount;
@@ -58,11 +64,15 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider {
58 return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt); 64 return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt);
59 } 65 }
60 66
67 public ScheduledExecutorService getScheduled(String key) {
68 return scheduledInstanceCache.computeIfAbsent(key, this::createScheduledInstance);
69 }
70
61 @Override 71 @Override
62 protected ExecutorService createInstance(String key) { 72 protected ExecutorService createInstance(String key) {
63 String name = "xtext-" + POOL_ID.getAndIncrement(); 73 String name = "xtext-" + POOL_ID.getAndIncrement();
64 if (key != null) { 74 if (key != null) {
65 name = name + key + "-"; 75 name = name + "-" + key;
66 } 76 }
67 var threadFactory = new Factory(name, 5); 77 var threadFactory = new Factory(name, 5);
68 int size = getSize(key); 78 int size = getSize(key);
@@ -72,6 +82,15 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider {
72 return Executors.newFixedThreadPool(size, threadFactory); 82 return Executors.newFixedThreadPool(size, threadFactory);
73 } 83 }
74 84
85 protected ScheduledExecutorService createScheduledInstance(String key) {
86 String name = "xtext-scheduled-" + POOL_ID.getAndIncrement();
87 if (key != null) {
88 name = name + "-" + key;
89 }
90 var threadFactory = new Factory(name, 5);
91 return Executors.newScheduledThreadPool(1, threadFactory);
92 }
93
75 private int getSize(String key) { 94 private int getSize(String key) {
76 if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) { 95 if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) {
77 return semanticsExecutorThreadCount; 96 return semanticsExecutorThreadCount;
@@ -82,6 +101,17 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider {
82 } 101 }
83 } 102 }
84 103
104 @Override
105 public void dispose() {
106 super.dispose();
107 synchronized (scheduledInstanceCache) {
108 for (var instance : scheduledInstanceCache.values()) {
109 instance.shutdown();
110 }
111 scheduledInstanceCache.clear();
112 }
113 }
114
85 private static class Factory implements ThreadFactory { 115 private static class Factory implements ThreadFactory {
86 // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}. 116 // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}.
87 @SuppressWarnings("squid:S3014") 117 @SuppressWarnings("squid:S3014")
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 74456604..a3792bac 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
@@ -187,7 +187,7 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
187 var document = subscription.get(); 187 var document = subscription.get();
188 if (document != null) { 188 if (document != null) {
189 document.removePrecomputationListener(this); 189 document.removePrecomputationListener(this);
190 document.cancelBackgroundWork(); 190 document.dispose();
191 } 191 }
192 } 192 }
193 } 193 }
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 d4a8c433..a04ee226 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,14 +5,17 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.push; 6package tools.refinery.language.web.xtext.server.push;
7 7
8import com.google.common.base.Optional;
8import com.google.inject.Inject; 9import com.google.inject.Inject;
9import org.eclipse.xtext.web.server.IServiceContext; 10import org.eclipse.xtext.web.server.IServiceContext;
11import org.eclipse.xtext.web.server.InvalidRequestException;
10import org.eclipse.xtext.web.server.XtextServiceDispatcher; 12import org.eclipse.xtext.web.server.XtextServiceDispatcher;
11import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; 13import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry;
12import org.eclipse.xtext.web.server.model.XtextWebDocument; 14import org.eclipse.xtext.web.server.model.XtextWebDocument;
13 15
14import com.google.inject.Singleton; 16import com.google.inject.Singleton;
15 17
18import tools.refinery.language.web.generator.ModelGenerationService;
16import tools.refinery.language.web.semantics.SemanticsService; 19import tools.refinery.language.web.semantics.SemanticsService;
17import tools.refinery.language.web.xtext.server.SubscribingServiceContext; 20import tools.refinery.language.web.xtext.server.SubscribingServiceContext;
18 21
@@ -21,6 +24,9 @@ public class PushServiceDispatcher extends XtextServiceDispatcher {
21 @Inject 24 @Inject
22 private SemanticsService semanticsService; 25 private SemanticsService semanticsService;
23 26
27 @Inject
28 private ModelGenerationService modelGenerationService;
29
24 @Override 30 @Override
25 @Inject 31 @Inject
26 protected void registerPreComputedServices(PrecomputedServiceRegistry registry) { 32 protected void registerPreComputedServices(PrecomputedServiceRegistry registry) {
@@ -37,4 +43,37 @@ public class PushServiceDispatcher extends XtextServiceDispatcher {
37 } 43 }
38 return document; 44 return document;
39 } 45 }
46
47 @Override
48 protected ServiceDescriptor createServiceDescriptor(String serviceType, IServiceContext context) {
49 if (ModelGenerationService.SERVICE_NAME.equals(serviceType)) {
50 return getModelGenerationService(context);
51 }
52 return super.createServiceDescriptor(serviceType, context);
53 }
54
55 protected ServiceDescriptor getModelGenerationService(IServiceContext context) throws InvalidRequestException {
56 var document = (PushWebDocumentAccess) getDocumentAccess(context);
57 // Using legacy Guava methods because of the Xtext dependency.
58 @SuppressWarnings({"Guava", "squid:S4738"})
59 boolean start = getBoolean(context, "start", Optional.of(false));
60 @SuppressWarnings({"Guava", "squid:S4738"})
61 boolean cancel = getBoolean(context, "cancel", Optional.of(false));
62 if (!start && !cancel) {
63 throw new InvalidRequestException("Either start of cancel must be specified");
64 }
65 var descriptor = new ServiceDescriptor();
66 descriptor.setService(() -> {
67 try {
68 if (start) {
69 return modelGenerationService.generateModel(document);
70 } else {
71 return modelGenerationService.cancelModelGeneration(document);
72 }
73 } catch (RuntimeException e) {
74 return handleError(descriptor, e);
75 }
76 });
77 return descriptor;
78 }
40} 79}
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 2d43fb26..ca97147a 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
@@ -13,19 +13,18 @@ import org.eclipse.xtext.web.server.model.DocumentSynchronizer;
13import org.eclipse.xtext.web.server.model.XtextWebDocument; 13import org.eclipse.xtext.web.server.model.XtextWebDocument;
14import org.slf4j.Logger; 14import org.slf4j.Logger;
15import org.slf4j.LoggerFactory; 15import org.slf4j.LoggerFactory;
16import tools.refinery.language.web.generator.ModelGenerationManager;
16import tools.refinery.language.web.xtext.server.ResponseHandlerException; 17import tools.refinery.language.web.xtext.server.ResponseHandlerException;
17 18
18import java.util.ArrayList; 19import java.util.ArrayList;
19import java.util.HashMap;
20import java.util.List; 20import java.util.List;
21import java.util.Map;
22 21
23public class PushWebDocument extends XtextWebDocument { 22public class PushWebDocument extends XtextWebDocument {
24 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class); 23 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class);
25 24
26 private final List<PrecomputationListener> precomputationListeners = new ArrayList<>(); 25 private final List<PrecomputationListener> precomputationListeners = new ArrayList<>();
27 26
28 private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>(); 27 private final ModelGenerationManager modelGenerationManager = new ModelGenerationManager();
29 28
30 private final DocumentSynchronizer synchronizer; 29 private final DocumentSynchronizer synchronizer;
31 30
@@ -34,6 +33,10 @@ public class PushWebDocument extends XtextWebDocument {
34 this.synchronizer = synchronizer; 33 this.synchronizer = synchronizer;
35 } 34 }
36 35
36 public ModelGenerationManager getModelGenerationManager() {
37 return modelGenerationManager;
38 }
39
37 public void addPrecomputationListener(PrecomputationListener listener) { 40 public void addPrecomputationListener(PrecomputationListener listener) {
38 synchronized (precomputationListeners) { 41 synchronized (precomputationListeners) {
39 if (precomputationListeners.contains(listener)) { 42 if (precomputationListeners.contains(listener)) {
@@ -52,15 +55,13 @@ public class PushWebDocument extends XtextWebDocument {
52 55
53 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, 56 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName,
54 CancelIndicator cancelIndicator, boolean logCacheMiss) { 57 CancelIndicator cancelIndicator, boolean logCacheMiss) {
55 var serviceClass = service.getClass();
56 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); 58 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss);
57 precomputedServices.put(serviceClass, result);
58 if (result != null) { 59 if (result != null) {
59 notifyPrecomputationListeners(serviceName, result); 60 notifyPrecomputationListeners(serviceName, result);
60 } 61 }
61 } 62 }
62 63
63 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { 64 public <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) {
64 var resourceId = getResourceId(); 65 var resourceId = getResourceId();
65 if (resourceId == null) { 66 if (resourceId == null) {
66 return; 67 return;
@@ -86,7 +87,12 @@ public class PushWebDocument extends XtextWebDocument {
86 } 87 }
87 } 88 }
88 89
89 public void cancelBackgroundWork() { 90 public void cancelModelGeneration() {
91 modelGenerationManager.cancel();
92 }
93
94 public void dispose() {
90 synchronizer.setCanceled(true); 95 synchronizer.setCanceled(true);
96 modelGenerationManager.dispose();
91 } 97 }
92} 98}
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 c72e8e67..1e68b244 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
@@ -74,4 +74,8 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess {
74 } 74 }
75 throw new IllegalArgumentException("Unknown precomputed service: " + service); 75 throw new IllegalArgumentException("Unknown precomputed service: " + service);
76 } 76 }
77
78 public void cancelModelGeneration() {
79 pushDocument.cancelModelGeneration();
80 }
77} 81}
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java
index 1dda7ac1..7f0ef8b4 100644
--- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java
@@ -47,6 +47,8 @@ public interface ReasoningAdapter extends ModelAdapter {
47 47
48 boolean cleanup(int nodeToDelete); 48 boolean cleanup(int nodeToDelete);
49 49
50 int getNodeCount();
51
50 static ReasoningBuilder builder() { 52 static ReasoningBuilder builder() {
51 return new ReasoningBuilderImpl(); 53 return new ReasoningBuilderImpl();
52 } 54 }
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java
index f91fdd07..bd16bdfa 100644
--- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java
@@ -204,4 +204,10 @@ class ReasoningAdapterImpl implements ReasoningAdapter {
204 } 204 }
205 return true; 205 return true;
206 } 206 }
207
208 @Override
209 public int getNodeCount() {
210 Integer nodeCount = nodeCountInterpretation.get(Tuple.of());
211 return nodeCount == null ? 0 : nodeCount;
212 }
207} 213}