From eb5da232b5954895b449957c73e35d0b36e3a902 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 17 Aug 2023 02:32:26 +0200 Subject: feat: basic semantics mapping and visualization --- subprojects/frontend/package.json | 2 + subprojects/frontend/src/Refinery.tsx | 140 ++++++++- subprojects/frontend/src/editor/EditorPane.tsx | 2 +- subprojects/frontend/src/editor/EditorStore.ts | 6 + subprojects/frontend/src/graph/GraphPane.tsx | 28 ++ subprojects/frontend/src/xtext/XtextClient.ts | 5 +- subprojects/frontend/src/xtext/xtextMessages.ts | 6 +- subprojects/language-semantics/build.gradle.kts | 2 + .../language/semantics/model/ModelInitializer.java | 337 ++++++++++++++++++--- .../language/semantics/model/SemanticsUtils.java | 31 ++ .../semantics/model/internal/DecisionTree.java | 20 +- .../model/internal/DecisionTreeCursor.java | 9 + .../semantics/model/tests/DecisionTreeTests.java | 11 + subprojects/language-web/build.gradle.kts | 2 + .../web/semantics/SemanticsErrorResult.java | 9 + .../language/web/semantics/SemanticsResult.java | 11 + .../language/web/semantics/SemanticsService.java | 155 ++++++++++ .../web/semantics/SemanticsSuccessResult.java | 13 + .../web/xtext/server/TransactionExecutor.java | 2 +- .../xtext/server/message/XtextWebOkResponse.java | 6 +- .../web/xtext/server/message/XtextWebResponse.java | 2 +- .../xtext/server/push/PushServiceDispatcher.java | 12 + .../web/xtext/server/push/PushWebDocument.java | 36 +-- .../xtext/server/push/PushWebDocumentAccess.java | 8 +- .../ProblemWebSocketServletIntegrationTest.java | 16 +- .../web/tests/RestartableCachedThreadPool.java | 2 +- .../web/xtext/servlet/TransactionExecutorTest.java | 4 +- .../tools/refinery/language/utils/ProblemUtil.java | 4 + yarn.lock | 23 +- 29 files changed, 819 insertions(+), 85 deletions(-) create mode 100644 subprojects/frontend/src/graph/GraphPane.tsx create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/SemanticsUtils.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsErrorResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java 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 @@ "ansi-styles": "^6.2.1", "csstype": "^3.1.2", "escape-string-regexp": "^5.0.0", + "json-stringify-pretty-compact": "^4.0.0", "lodash-es": "^4.17.21", "loglevel": "^1.8.1", "loglevel-plugin-prefix": "^0.8.4", @@ -59,6 +60,7 @@ "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-resize-detector": "^9.1.0", "xstate": "^4.38.2", "zod": "^3.22.0" }, 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 @@ * SPDX-License-Identifier: EPL-2.0 */ +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import Box from '@mui/material/Box'; import Grow from '@mui/material/Grow'; import Stack from '@mui/material/Stack'; +import { alpha, useTheme } from '@mui/material/styles'; import { SnackbarProvider } from 'notistack'; +import { memo, useRef, useState } from 'react'; +import { useResizeDetector } from 'react-resize-detector'; import TopBar from './TopBar'; import UpdateNotification from './UpdateNotification'; import EditorPane from './editor/EditorPane'; +import GraphPane from './graph/GraphPane'; + +const DirectionalSplitPane = memo(function SplitPanel({ + horizontalSplit, +}: { + horizontalSplit: boolean; +}): JSX.Element { + const theme = useTheme(); + const stackRef = useRef(null); + const sliderRef = useRef(null); + const [resizing, setResizing] = useState(false); + const [fraction, setFraction] = useState(0.5); + + const direction = horizontalSplit ? 'column' : 'row'; + const axis = horizontalSplit ? 'height' : 'width'; + const primarySize = `calc(${fraction * 100}% - 0.5px)`; + const secondarySize = `calc(${(1 - fraction) * 100}% - 0.5px)`; + + return ( + + + + + + { + if (event.button !== 0) { + return; + } + sliderRef.current?.setPointerCapture(event.pointerId); + setResizing(true); + }} + onPointerUp={(event) => { + if (event.button !== 0) { + return; + } + sliderRef.current?.releasePointerCapture(event.pointerId); + setResizing(false); + }} + onPointerMove={(event) => { + if (!resizing) { + return; + } + const container = stackRef.current; + if (container === null) { + return; + } + const rect = container.getBoundingClientRect(); + const newFraction = horizontalSplit + ? (event.clientY - rect.top) / rect.height + : (event.clientX - rect.left) / rect.width; + setFraction(Math.min(0.9, Math.max(0.1, newFraction))); + }} + onDoubleClick={() => setFraction(0.5)} + > + {horizontalSplit ? : } + + + + + + + ); +}); + +function SplitPane(): JSX.Element { + const { ref, width, height } = useResizeDetector(); + const horizontalSplit = + width !== undefined && height !== undefined && height > width; + + return ( + + + + ); +} export default function Refinery(): JSX.Element { return ( @@ -18,7 +156,7 @@ export default function Refinery(): JSX.Element { - + ); 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 { const { editorStore } = useRootStore(); return ( - + 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 { disposed = false; + semantics: unknown = {}; + constructor(initialValue: string, pwaStore: PWAStore) { this.id = nanoid(); this.state = createEditorState(initialValue, this); @@ -282,6 +284,10 @@ export default class EditorStore { return true; } + setSemantics(semantics: unknown) { + this.semantics = semantics; + } + dispose(): void { this.client?.dispose(); 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 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import Stack from '@mui/material/Stack'; +import { styled } from '@mui/material/styles'; +import stringify from 'json-stringify-pretty-compact'; +import { observer } from 'mobx-react-lite'; + +import { useRootStore } from '../RootStoreProvider'; + +const StyledCode = styled('code')(({ theme }) => ({ + ...theme.typography.editor, + fontWeight: theme.typography.fontWeightEditorNormal, + margin: theme.spacing(2), + whiteSpace: 'pre', +})); + +export default observer(function GraphPane(): JSX.Element { + const { editorStore } = useRootStore(); + return ( + + {stringify(editorStore?.semantics ?? {})} + + ); +}); 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 { private readonly occurrencesService: OccurrencesService; constructor( - store: EditorStore, + private readonly store: EditorStore, private readonly pwaStore: PWAStore, ) { this.webSocketClient = new XtextWebSocketClient( @@ -114,6 +114,9 @@ export default class XtextClient { case 'validate': this.validationService.onPush(push); return; + case 'semantics': + this.store.setSemantics(push); + return; default: throw new Error('Unknown service'); } 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({ export type XtextWebErrorResponse = z.infer; -export const XtextWebPushService = z.enum(['highlight', 'validate']); +export const XtextWebPushService = z.enum([ + 'highlight', + 'validate', + 'semantics', +]); export type XtextWebPushService = z.infer; 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 { implementation(libs.eclipseCollections.api) api(project(":refinery-language")) api(project(":refinery-store")) + api(project(":refinery-store-query")) + api(project(":refinery-store-reasoning")) testImplementation(testFixtures(project(":refinery-language"))) } 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; import org.eclipse.collections.api.factory.primitive.ObjectIntMaps; import org.eclipse.collections.api.map.primitive.MutableObjectIntMap; import tools.refinery.language.model.problem.*; +import tools.refinery.language.semantics.model.internal.DecisionTree; +import tools.refinery.language.utils.BuiltinSymbols; import tools.refinery.language.utils.ProblemDesugarer; -import tools.refinery.store.representation.Symbol; +import tools.refinery.language.utils.ProblemUtil; +import tools.refinery.store.model.ModelStoreBuilder; +import tools.refinery.store.reasoning.ReasoningAdapter; +import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.seed.ModelSeed; +import tools.refinery.store.reasoning.seed.Seed; +import tools.refinery.store.reasoning.translator.containment.ContainmentHierarchyTranslator; +import tools.refinery.store.reasoning.translator.metamodel.Metamodel; +import tools.refinery.store.reasoning.translator.metamodel.MetamodelBuilder; +import tools.refinery.store.reasoning.translator.metamodel.MetamodelTranslator; +import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; +import tools.refinery.store.reasoning.translator.multiplicity.ConstrainedMultiplicity; +import tools.refinery.store.reasoning.translator.multiplicity.Multiplicity; +import tools.refinery.store.reasoning.translator.multiplicity.UnconstrainedMultiplicity; import tools.refinery.store.representation.TruthValue; +import tools.refinery.store.representation.cardinality.CardinalityInterval; +import tools.refinery.store.representation.cardinality.CardinalityIntervals; +import tools.refinery.store.representation.cardinality.UpperCardinalities; import tools.refinery.store.tuple.Tuple; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.Map; public class ModelInitializer { @Inject private ProblemDesugarer desugarer; + @Inject + private SemanticsUtils semanticsUtils; + + private Problem problem; + + private BuiltinSymbols builtinSymbols; + + private PartialRelation nodeRelation; + private final MutableObjectIntMap nodeTrace = ObjectIntMaps.mutable.empty(); - private final Map> relationTrace = - new HashMap<>(); + private final Map relationInfoMap = new LinkedHashMap<>(); - private int nodeCount = 0; + private Map relationTrace; + + private final MetamodelBuilder metamodelBuilder = Metamodel.builder(); + + public int getNodeCount() { + return nodeTrace.size(); + } + + public MutableObjectIntMap getNodeTrace() { + return nodeTrace; + } - /*public void createModel(Problem problem) { - var builtinSymbols = desugarer.getBuiltinSymbols(problem).orElseThrow(() -> new IllegalArgumentException( + public Map getRelationTrace() { + return relationTrace; + } + + public ModelSeed createModel(Problem problem, ModelStoreBuilder builder) { + this.problem = problem; + builtinSymbols = desugarer.getBuiltinSymbols(problem).orElseThrow(() -> new IllegalArgumentException( "Problem has no builtin library")); - var collectedSymbols = desugarer.collectSymbols(problem); - for (var node : collectedSymbols.nodes().keySet()) { - nodeTrace.put(node, nodeCount); - nodeCount += 1; - } - for (var pair : collectedSymbols.relations().entrySet()) { - var relation = pair.getKey(); - var relationInfo = pair.getValue(); - var isEqualsRelation = relation == builtinSymbols.equals(); - var decisionTree = mergeAssertions(relationInfo, isEqualsRelation); - var defaultValue = isEqualsRelation ? TruthValue.FALSE : TruthValue.UNKNOWN; - relationTrace.put(relation, Symbol.of( - relationInfo.name(), relationInfo.arity(), TruthValue.class, defaultValue)); - } - } - - private DecisionTree mergeAssertions(RelationInfo relationInfo, boolean isEqualsRelation) { - var arity = relationInfo.arity(); - var defaultAssertions = new DecisionTree(arity, isEqualsRelation ? null : TruthValue.UNKNOWN); - var assertions = new DecisionTree(arity); - for (var assertion : relationInfo.assertions()) { - var tuple = getTuple(assertion); - var value = getTruthValue(assertion.getValue()); - if (assertion.isDefault()) { - defaultAssertions.mergeValue(tuple, value); + var nodeInfo = collectPartialRelation(builtinSymbols.node(), 1, TruthValue.TRUE, TruthValue.TRUE); + nodeRelation = nodeInfo.partialRelation(); + metamodelBuilder.type(nodeRelation, true); + relationInfoMap.put(builtinSymbols.exists(), new RelationInfo(ReasoningAdapter.EXISTS_SYMBOL, null, + TruthValue.TRUE)); + relationInfoMap.put(builtinSymbols.equals(), new RelationInfo(ReasoningAdapter.EQUALS_SYMBOL, (TruthValue) null, + null)); + relationInfoMap.put(builtinSymbols.contained(), new RelationInfo(ContainmentHierarchyTranslator.CONTAINED_SYMBOL, + null, TruthValue.UNKNOWN)); + relationInfoMap.put(builtinSymbols.contains(), new RelationInfo(ContainmentHierarchyTranslator.CONTAINS_SYMBOL, + null, TruthValue.UNKNOWN)); + relationInfoMap.put(builtinSymbols.invalidNumberOfContainers(), + new RelationInfo(ContainmentHierarchyTranslator.INVALID_NUMBER_OF_CONTAINERS, TruthValue.FALSE, + TruthValue.FALSE)); + collectNodes(); + collectPartialSymbols(); + collectAssertions(); + var metamodel = metamodelBuilder.build(); + builder.with(ReasoningAdapter.builder()); + builder.with(new MultiObjectTranslator()); + builder.with(new MetamodelTranslator(metamodel)); + relationTrace = new LinkedHashMap<>(relationInfoMap.size()); + int nodeCount = getNodeCount(); + var modelSeedBuilder = ModelSeed.builder(nodeCount); + for (var entry : relationInfoMap.entrySet()) { + var relation = entry.getKey(); + var info = entry.getValue(); + var partialRelation = info.partialRelation(); + relationTrace.put(relation, partialRelation); + modelSeedBuilder.seed(partialRelation, info.toSeed(nodeCount)); + } + return modelSeedBuilder.build(); + } + + private void collectNodes() { + for (var statement : problem.getStatements()) { + if (statement instanceof IndividualDeclaration individualDeclaration) { + for (var individual : individualDeclaration.getNodes()) { + collectNode(individual); + } + } else if (statement instanceof ClassDeclaration classDeclaration) { + var newNode = classDeclaration.getNewNode(); + if (newNode != null) { + collectNode(newNode); + } + } else if (statement instanceof EnumDeclaration enumDeclaration) { + for (var literal : enumDeclaration.getLiterals()) { + collectNode(literal); + } + } + } + for (var node : problem.getNodes()) { + collectNode(node); + } + } + + private void collectNode(Node node) { + nodeTrace.getIfAbsentPut(node, this::getNodeCount); + } + + private void collectPartialSymbols() { + for (var statement : problem.getStatements()) { + if (statement instanceof ClassDeclaration classDeclaration) { + collectClassDeclaration(classDeclaration); + } else if (statement instanceof EnumDeclaration enumDeclaration) { + collectPartialRelation(enumDeclaration, 1, null, TruthValue.FALSE); + } else if (statement instanceof PredicateDefinition predicateDefinition) { + // TODO Implement predicate definitions + } + } + } + + private void collectClassDeclaration(ClassDeclaration classDeclaration) { + collectPartialRelation(classDeclaration, 1, null, TruthValue.UNKNOWN); + for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { + if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) { + collectPartialRelation(referenceDeclaration, 2, null, TruthValue.UNKNOWN); + var invalidMultiplicityConstraint = referenceDeclaration.getInvalidMultiplicity(); + if (invalidMultiplicityConstraint != null) { + collectPartialRelation(invalidMultiplicityConstraint, 1, TruthValue.FALSE, TruthValue.FALSE); + } } else { - assertions.mergeValue(tuple, value); + throw new IllegalArgumentException("Unknown feature declaration: " + featureDeclaration); + } + } + } + + private RelationInfo collectPartialRelation(Relation relation, int arity, TruthValue value, + TruthValue defaultValue) { + return relationInfoMap.computeIfAbsent(relation, key -> { + var name = getName(relation); + return new RelationInfo(name, arity, value, defaultValue); + }); + } + + private String getName(Relation relation) { + return semanticsUtils.getName(relation).orElseGet(() -> "#" + relationInfoMap.size()); + } + + private void collectAssertions() { + for (var statement : problem.getStatements()) { + if (statement instanceof ClassDeclaration classDeclaration) { + collectClassDeclarationAssertions(classDeclaration); + } else if (statement instanceof EnumDeclaration enumDeclaration) { + collectEnumAssertions(enumDeclaration); + } else if (statement instanceof IndividualDeclaration individualDeclaration) { + for (var individual : individualDeclaration.getNodes()) { + collectIndividualAssertions(individual); + } + } else if (statement instanceof Assertion assertion) { + collectAssertion(assertion); } } - defaultAssertions.overwriteValues(assertions); - if (isEqualsRelation) { - for (int i = 0; i < nodeCount; i++) { - defaultAssertions.setIfMissing(Tuple.of(i, i), TruthValue.TRUE); + } + + private void collectClassDeclarationAssertions(ClassDeclaration classDeclaration) { + var superTypes = classDeclaration.getSuperTypes(); + var partialSuperTypes = new ArrayList(superTypes.size() + 1); + partialSuperTypes.add(nodeRelation); + for (var superType : superTypes) { + partialSuperTypes.add(getRelationInfo(superType).partialRelation()); + } + var info = getRelationInfo(classDeclaration); + metamodelBuilder.type(info.partialRelation(), classDeclaration.isAbstract(), + partialSuperTypes); + var newNode = classDeclaration.getNewNode(); + if (newNode != null) { + var newNodeId = getNodeId(newNode); + collectCardinalityAssertions(newNodeId, TruthValue.UNKNOWN); + mergeValue(classDeclaration, Tuple.of(newNodeId), TruthValue.TRUE); + } + for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { + if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) { + collectReferenceDeclarationAssertions(classDeclaration, referenceDeclaration); + } else { + throw new IllegalArgumentException("Unknown feature declaration: " + featureDeclaration); } - defaultAssertions.setAllMissing(TruthValue.FALSE); } - return defaultAssertions; - }*/ + } + + private void collectReferenceDeclarationAssertions(ClassDeclaration classDeclaration, + ReferenceDeclaration referenceDeclaration) { + var relation = getRelationInfo(referenceDeclaration).partialRelation(); + var source = getRelationInfo(classDeclaration).partialRelation(); + var target = getRelationInfo(referenceDeclaration.getReferenceType()).partialRelation(); + boolean containment = referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT; + var opposite = referenceDeclaration.getOpposite(); + PartialRelation oppositeRelation = null; + if (opposite != null) { + oppositeRelation = getRelationInfo(opposite).partialRelation(); + } + var multiplicity = getMultiplicityConstraint(referenceDeclaration); + metamodelBuilder.reference(relation, source, containment, multiplicity, target, oppositeRelation); + } + + private Multiplicity getMultiplicityConstraint(ReferenceDeclaration referenceDeclaration) { + if (!ProblemUtil.hasMultiplicityConstraint(referenceDeclaration)) { + return UnconstrainedMultiplicity.INSTANCE; + } + var problemMultiplicity = referenceDeclaration.getMultiplicity(); + CardinalityInterval interval; + if (problemMultiplicity == null) { + interval = CardinalityIntervals.LONE; + } else if (problemMultiplicity instanceof ExactMultiplicity exactMultiplicity) { + interval = CardinalityIntervals.exactly(exactMultiplicity.getExactValue()); + } else if (problemMultiplicity instanceof RangeMultiplicity rangeMultiplicity) { + var upperBound = rangeMultiplicity.getUpperBound(); + interval = CardinalityIntervals.between(rangeMultiplicity.getLowerBound(), + upperBound < 0 ? UpperCardinalities.UNBOUNDED : UpperCardinalities.atMost(upperBound)); + } else { + throw new IllegalArgumentException("Unknown multiplicity: " + problemMultiplicity); + } + var constraint = getRelationInfo(referenceDeclaration.getInvalidMultiplicity()).partialRelation(); + return ConstrainedMultiplicity.of(interval, constraint); + } + + private void collectEnumAssertions(EnumDeclaration enumDeclaration) { + var info = getRelationInfo(enumDeclaration); + metamodelBuilder.type(info.partialRelation(), nodeRelation); + var overlay = new DecisionTree(1, null); + for (var literal : enumDeclaration.getLiterals()) { + collectIndividualAssertions(literal); + var nodeId = getNodeId(literal); + overlay.mergeValue(Tuple.of(nodeId), TruthValue.TRUE); + } + info.assertions().overwriteValues(overlay); + } + + private void collectIndividualAssertions(Node node) { + var nodeId = getNodeId(node); + collectCardinalityAssertions(nodeId, TruthValue.TRUE); + } + + private void collectCardinalityAssertions(int nodeId, TruthValue value) { + mergeValue(builtinSymbols.exists(), Tuple.of(nodeId), value); + mergeValue(builtinSymbols.equals(), Tuple.of(nodeId, nodeId), value); + } + + private void collectAssertion(Assertion assertion) { + var relation = assertion.getRelation(); + var tuple = getTuple(assertion); + var value = getTruthValue(assertion.getValue()); + if (assertion.isDefault()) { + mergeDefaultValue(relation, tuple, value); + } else { + mergeValue(relation, tuple, value); + } + } + + private void mergeValue(Relation relation, Tuple key, TruthValue value) { + getRelationInfo(relation).assertions().mergeValue(key, value); + } + + private void mergeDefaultValue(Relation relation, Tuple key, TruthValue value) { + getRelationInfo(relation).defaultAssertions().mergeValue(key, value); + } + + private RelationInfo getRelationInfo(Relation relation) { + var info = relationInfoMap.get(relation); + if (info == null) { + throw new IllegalArgumentException("Unknown relation: " + relation); + } + return info; + } + + private int getNodeId(Node node) { + return nodeTrace.getOrThrow(node); + } private Tuple getTuple(Assertion assertion) { var arguments = assertion.getArguments(); @@ -77,7 +309,7 @@ public class ModelInitializer { for (int i = 0; i < arity; i++) { var argument = arguments.get(i); if (argument instanceof NodeAssertionArgument nodeArgument) { - nodes[i] = nodeTrace.getOrThrow(nodeArgument.getNode()); + nodes[i] = getNodeId(nodeArgument.getNode()); } else if (argument instanceof WildcardAssertionArgument) { nodes[i] = -1; } else { @@ -98,4 +330,27 @@ public class ModelInitializer { case ERROR -> TruthValue.ERROR; }; } + + private record RelationInfo(PartialRelation partialRelation, DecisionTree assertions, + DecisionTree defaultAssertions) { + public RelationInfo(String name, int arity, TruthValue value, TruthValue defaultValue) { + this(new PartialRelation(name, arity), value, defaultValue); + } + + public RelationInfo(PartialRelation partialRelation, TruthValue value, TruthValue defaultValue) { + this(partialRelation, new DecisionTree(partialRelation.arity(), value), + new DecisionTree(partialRelation.arity(), defaultValue)); + } + + public Seed toSeed(int nodeCount) { + defaultAssertions.overwriteValues(assertions); + if (partialRelation.equals(ReasoningAdapter.EQUALS_SYMBOL)) { + for (int i = 0; i < nodeCount; i++) { + defaultAssertions.setIfMissing(Tuple.of(i, i), TruthValue.TRUE); + } + defaultAssertions.setAllMissing(TruthValue.FALSE); + } + return defaultAssertions; + } + } } 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 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.model; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.naming.IQualifiedNameProvider; + +import java.util.Optional; + +@Singleton +public class SemanticsUtils { + @Inject + private IQualifiedNameProvider qualifiedNameProvider; + + @Inject + private IQualifiedNameConverter qualifiedNameConverter; + + public Optional getName(EObject eObject) { + var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(eObject); + if (qualifiedName == null) { + return Optional.empty(); + } + return Optional.of(qualifiedNameConverter.toString(qualifiedName)); + } +} 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; import org.eclipse.collections.api.factory.primitive.IntObjectMaps; import tools.refinery.store.map.Cursor; +import tools.refinery.store.reasoning.seed.Seed; import tools.refinery.store.tuple.Tuple; import tools.refinery.store.representation.TruthValue; -public class DecisionTree { +public class DecisionTree implements Seed { private final int levels; private final DecisionTreeNode root; @@ -29,6 +30,22 @@ public class DecisionTree { this(levels, null); } + @Override + public int arity() { + return levels; + } + + @Override + public Class valueType() { + return TruthValue.class; + } + + @Override + public TruthValue reducedValue() { + return root.getReducedValue().getTruthValue(); + } + + @Override public TruthValue get(Tuple tuple) { return root.getValue(levels - 1, tuple).getTruthValue(); } @@ -60,6 +77,7 @@ public class DecisionTree { return reducedValue == null ? null : reducedValue.getTruthValue(); } + @Override public Cursor getCursor(TruthValue defaultValue, int nodeCount) { return new DecisionTreeCursor(levels, defaultValue, nodeCount, root); } 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 { @Override public boolean move() { + while (moveOne()) { + if (!value.equals(defaultValue)) { + return true; + } + } + return false; + } + + private boolean moveOne() { boolean found = false; if (path.isEmpty() && !terminated) { 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 @@ -133,6 +133,17 @@ class DecisionTreeTests { assertThat(map, hasEntry(Tuple.of(1, 1), TruthValue.FALSE)); } + @Test + void overwriteIterationTest() { + var sut = new DecisionTree(1, TruthValue.TRUE); + var overwrite = new DecisionTree(1, null); + overwrite.mergeValue(Tuple.of(0), TruthValue.UNKNOWN); + sut.overwriteValues(overwrite); + var map = iterateAll(sut, TruthValue.UNKNOWN, 2); + assertThat(map.keySet(), hasSize(1)); + assertThat(map, hasEntry(Tuple.of(1), TruthValue.TRUE)); + } + @Test void overwriteNothingTest() { var sut = new DecisionTree(2, TruthValue.UNKNOWN); 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 { dependencies { implementation(project(":refinery-language")) implementation(project(":refinery-language-ide")) + implementation(project(":refinery-language-semantics")) + implementation(project(":refinery-store-query-viatra")) implementation(libs.jetty.server) implementation(libs.jetty.servlet) 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 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.semantics; + +public record SemanticsErrorResult(String error) implements SemanticsResult { +} 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 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.semantics; + +import org.eclipse.xtext.web.server.IServiceResult; + +public sealed interface SemanticsResult extends IServiceResult permits SemanticsSuccessResult, SemanticsErrorResult { +} 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 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.semantics; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.eclipse.xtext.service.OperationCanceledManager; +import org.eclipse.xtext.util.CancelIndicator; +import org.eclipse.xtext.web.server.model.AbstractCachedService; +import org.eclipse.xtext.web.server.model.IXtextWebDocument; +import org.eclipse.xtext.web.server.model.XtextWebDocument; +import org.eclipse.xtext.web.server.validation.ValidationService; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tools.refinery.language.model.problem.Problem; +import tools.refinery.language.semantics.model.ModelInitializer; +import tools.refinery.language.semantics.model.SemanticsUtils; +import tools.refinery.store.model.Model; +import tools.refinery.store.model.ModelStore; +import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; +import tools.refinery.store.reasoning.ReasoningAdapter; +import tools.refinery.store.reasoning.ReasoningStoreAdapter; +import tools.refinery.store.reasoning.literal.Concreteness; +import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.representation.TruthValue; +import tools.refinery.store.tuple.Tuple; + +import java.util.Arrays; +import java.util.List; +import java.util.TreeMap; + +@Singleton +public class SemanticsService extends AbstractCachedService { + private static final Logger LOG = LoggerFactory.getLogger(SemanticsService.class); + + @Inject + private SemanticsUtils semanticsUtils; + + @Inject + private ValidationService validationService; + + @Inject + private Provider initializerProvider; + + @Inject + private OperationCanceledManager operationCanceledManager; + + @Override + public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) { + long start = System.currentTimeMillis(); + Problem problem = getProblem(doc, cancelIndicator); + if (problem == null) { + return null; + } + var initializer = initializerProvider.get(); + var builder = ModelStore.builder() + .with(ViatraModelQueryAdapter.builder()); + operationCanceledManager.checkCanceled(cancelIndicator); + try { + var modelSeed = initializer.createModel(problem, builder); + operationCanceledManager.checkCanceled(cancelIndicator); + var nodeTrace = getNodeTrace(initializer); + operationCanceledManager.checkCanceled(cancelIndicator); + var store = builder.build(); + operationCanceledManager.checkCanceled(cancelIndicator); + var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); + operationCanceledManager.checkCanceled(cancelIndicator); + var partialInterpretation = getPartialInterpretation(initializer, model, cancelIndicator); + long end = System.currentTimeMillis(); + LOG.info("Computed semantics for {} ({}) in {}ms", doc.getResourceId(), doc.getStateId(), end - start); + return new SemanticsSuccessResult(nodeTrace, partialInterpretation); + } catch (RuntimeException e) { + LOG.error("Error while computing semantics", e); + return new SemanticsErrorResult(e.toString()); + } + } + + @Nullable + private Problem getProblem(IXtextWebDocument doc, CancelIndicator cancelIndicator) { + if (!(doc instanceof XtextWebDocument webDoc)) { + throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc); + } + var validationResult = webDoc.getCachedServiceResult(validationService, cancelIndicator, true); + boolean hasError = validationResult.getIssues().stream() + .anyMatch(issue -> "error".equals(issue.getSeverity())); + if (hasError) { + return null; + } + var contents = doc.getResource().getContents(); + if (contents.isEmpty()) { + return null; + } + var model = contents.get(0); + if (!(model instanceof Problem problem)) { + return null; + } + return problem; + } + + private List getNodeTrace(ModelInitializer initializer) { + var nodeTrace = new String[initializer.getNodeCount()]; + for (var entry : initializer.getNodeTrace().keyValuesView()) { + var node = entry.getOne(); + var index = entry.getTwo(); + nodeTrace[index] = semanticsUtils.getName(node).orElse(null); + } + return Arrays.asList(nodeTrace); + } + + private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model, + CancelIndicator cancelIndicator) { + var adapter = model.getAdapter(ReasoningAdapter.class); + var json = new JsonObject(); + for (var entry : initializer.getRelationTrace().entrySet()) { + var relation = entry.getKey(); + var partialSymbol = entry.getValue(); + var tuples = getTuplesJson(adapter, partialSymbol); + var name = semanticsUtils.getName(relation).orElse(partialSymbol.name()); + json.add(name, tuples); + operationCanceledManager.checkCanceled(cancelIndicator); + } + return json; + } + + private static JsonArray getTuplesJson(ReasoningAdapter adapter, PartialRelation partialSymbol) { + var interpretation = adapter.getPartialInterpretation(Concreteness.PARTIAL, partialSymbol); + var cursor = interpretation.getAll(); + var map = new TreeMap(); + while (cursor.move()) { + map.put(cursor.getKey(), cursor.getValue()); + } + var tuples = new JsonArray(); + for (var entry : map.entrySet()) { + tuples.add(toArray(entry.getKey(), entry.getValue())); + } + return tuples; + } + + private static JsonArray toArray(Tuple tuple, TruthValue value) { + int arity = tuple.getSize(); + var json = new JsonArray(arity + 1); + for (int i = 0; i < arity; i++) { + json.add(tuple.get(i)); + } + json.add(value.toString()); + return json; + } +} 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 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.semantics; + +import com.google.gson.JsonObject; + +import java.util.List; + +public record SemanticsSuccessResult(List nodes, JsonObject partialInterpretation) implements SemanticsResult { +} 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 * @throws UnknownLanguageException if the Xtext language cannot be determined */ protected Injector getInjector(IServiceContext context) { - IResourceServiceProvider resourceServiceProvider = null; + IResourceServiceProvider resourceServiceProvider; var resourceName = context.getParameter("resource"); if (resourceName == null) { 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 @@ */ package tools.refinery.language.web.xtext.server.message; -import java.util.Objects; - +import com.google.gson.annotations.SerializedName; import org.eclipse.xtext.web.server.IServiceResult; import org.eclipse.xtext.web.server.IUnwrappableServiceResult; -import com.google.gson.annotations.SerializedName; +import java.util.Objects; public final class XtextWebOkResponse implements XtextWebResponse { private String id; @@ -19,7 +18,6 @@ public final class XtextWebOkResponse implements XtextWebResponse { private Object responseData; public XtextWebOkResponse(String id, Object responseData) { - super(); this.id = id; this.responseData = responseData; } 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 @@ */ package tools.refinery.language.web.xtext.server.message; -public sealed interface XtextWebResponse permits XtextWebOkResponse,XtextWebErrorResponse,XtextWebPushMessage { +public sealed interface XtextWebResponse permits XtextWebOkResponse, XtextWebErrorResponse, XtextWebPushMessage { } 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 @@ */ package tools.refinery.language.web.xtext.server.push; +import com.google.inject.Inject; import org.eclipse.xtext.web.server.IServiceContext; import org.eclipse.xtext.web.server.XtextServiceDispatcher; +import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; import org.eclipse.xtext.web.server.model.XtextWebDocument; import com.google.inject.Singleton; +import tools.refinery.language.web.semantics.SemanticsService; import tools.refinery.language.web.xtext.server.SubscribingServiceContext; @Singleton public class PushServiceDispatcher extends XtextServiceDispatcher { + @Inject + private SemanticsService semanticsService; + + @Override + @Inject + protected void registerPreComputedServices(PrecomputedServiceRegistry registry) { + super.registerPreComputedServices(registry); + registry.addPrecomputedService(semanticsService); + } @Override 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 @@ */ package tools.refinery.language.web.xtext.server.push; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import com.google.common.collect.ImmutableList; import org.eclipse.xtext.util.CancelIndicator; import org.eclipse.xtext.web.server.IServiceResult; import org.eclipse.xtext.web.server.model.AbstractCachedService; @@ -17,11 +13,13 @@ import org.eclipse.xtext.web.server.model.DocumentSynchronizer; import org.eclipse.xtext.web.server.model.XtextWebDocument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import com.google.common.collect.ImmutableList; - import tools.refinery.language.web.xtext.server.ResponseHandlerException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + public class PushWebDocument extends XtextWebDocument { private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class); @@ -36,37 +34,31 @@ public class PushWebDocument extends XtextWebDocument { } } - public boolean addPrecomputationListener(PrecomputationListener listener) { + public void addPrecomputationListener(PrecomputationListener listener) { synchronized (precomputationListeners) { if (precomputationListeners.contains(listener)) { - return false; + return; } precomputationListeners.add(listener); listener.onSubscribeToPrecomputationEvents(getResourceId(), this); - return true; } } - public boolean removePrecomputationListener(PrecomputationListener listener) { + public void removePrecomputationListener(PrecomputationListener listener) { synchronized (precomputationListeners) { - return precomputationListeners.remove(listener); + precomputationListeners.remove(listener); } } public void precomputeServiceResult(AbstractCachedService service, String serviceName, CancelIndicator cancelIndicator, boolean logCacheMiss) { - var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); - if (result == null) { - LOG.error("{} service returned null result", serviceName); - return; - } var serviceClass = service.getClass(); var previousResult = precomputedServices.get(serviceClass); - if (previousResult != null && previousResult.equals(result)) { - return; - } + var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); precomputedServices.put(serviceClass, result); - notifyPrecomputationListeners(serviceName, result); + if (result != null && !result.equals(previousResult)) { + notifyPrecomputationListeners(serviceName, result); + } } private 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; import org.eclipse.xtext.web.server.validation.ValidationService; import com.google.inject.Inject; +import tools.refinery.language.web.semantics.SemanticsService; public class PushWebDocumentAccess extends XtextWebDocumentAccess { @@ -49,7 +50,7 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess { precomputeServiceResult(service, false); } } - + protected void precomputeServiceResult(AbstractCachedService service, boolean logCacheMiss) { var serviceName = getPrecomputedServiceName(service); readOnly(new CancelableUnitOfWork() { @@ -60,7 +61,7 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess { } }); } - + protected String getPrecomputedServiceName(AbstractCachedService service) { if (service instanceof ValidationService) { return "validate"; @@ -68,6 +69,9 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess { if (service instanceof HighlightingService) { return "highlight"; } + if (service instanceof SemanticsService) { + return "semantics"; + } throw new IllegalArgumentException("Unknown precomputed service: " + service); } } 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 { clientSocket.waitForTestResult(); assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); var responses = clientSocket.getResponses(); - assertThat(responses, hasSize(5)); + assertThat(responses, hasSize(7)); assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); assertThat(responses.get(1), startsWith( "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\"," + @@ -101,10 +101,16 @@ class ProblemWebSocketServletIntegrationTest { assertThat(responses.get(2), equalTo( "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"validate\"," + "\"push\":{\"issues\":[]}}")); - assertThat(responses.get(3), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}")); - assertThat(responses.get(4), startsWith( + assertThat(responses.get(3), startsWith( + "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"semantics\"," + + "\"push\":{")); + assertThat(responses.get(4), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}")); + assertThat(responses.get(5), startsWith( "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\"," + "\"push\":{\"regions\":[")); + assertThat(responses.get(6), startsWith( + "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"semantics\"," + + "\"push\":{")); } @WebSocket @@ -117,14 +123,14 @@ class ProblemWebSocketServletIntegrationTest { "\"fullText\":\"class Person.\n\"}}", Callback.NOOP ); - case 3 -> //noinspection TextBlockMigration + case 4 -> //noinspection TextBlockMigration session.sendText( "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\"," + "\"requiredStateId\":\"-80000000\",\"deltaText\":\"indiv q.\nnode(q).\n\"," + "\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}", Callback.NOOP ); - case 5 -> session.close(); + case 7 -> session.close(); } } } 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 { public void waitForTermination() { boolean result = false; try { - result = delegate.awaitTermination(1, TimeUnit.SECONDS); + result = delegate.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { LOG.warn("Interrupted while waiting for delegate executor to stop", e); } 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 { "0"))); var captor = newCaptor(); - verify(responseHandler, times(2)).onResponse(captor.capture()); + verify(responseHandler, times(3)).onResponse(captor.capture()); var newStateId = getStateId("bar", captor.getAllValues().get(0)); assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); } @@ -126,7 +126,7 @@ class TransactionExecutorTest { private String updateFullText(ArgumentCaptor captor) throws ResponseHandlerException { var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", UPDATE_FULL_TEXT_PARAMS)); - verify(responseHandler, times(3)).onResponse(captor.capture()); + verify(responseHandler, times(4)).onResponse(captor.capture()); return getStateId("foo", captor.getAllValues().get(0)); } 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 { } public static boolean hasMultiplicityConstraint(ReferenceDeclaration referenceDeclaration) { + var opposite = referenceDeclaration.getOpposite(); + if (opposite != null && opposite.getKind() == ReferenceKind.CONTAINMENT) { + return false; + } var multiplicity = referenceDeclaration.getMultiplicity(); if (multiplicity instanceof UnboundedMultiplicity) { return false; diff --git a/yarn.lock b/yarn.lock index 607cb0b3..bb806e86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2144,6 +2144,7 @@ __metadata: eslint-plugin-react: "npm:^7.33.1" eslint-plugin-react-hooks: "npm:^4.6.0" html-minifier-terser: "npm:^7.2.0" + json-stringify-pretty-compact: "npm:^4.0.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.8.1" loglevel-plugin-prefix: "npm:^0.8.4" @@ -2156,6 +2157,7 @@ __metadata: prettier: "npm:^3.0.1" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" + react-resize-detector: "npm:^9.1.0" typescript: "npm:5.1.6" vite: "npm:^4.4.9" vite-plugin-pwa: "npm:^0.16.4" @@ -5206,6 +5208,13 @@ __metadata: languageName: node linkType: hard +"json-stringify-pretty-compact@npm:^4.0.0": + version: 4.0.0 + resolution: "json-stringify-pretty-compact@npm:4.0.0" + checksum: 505781b4be7c72047ae8dfa667b520d20461ceac451b6516cb8ac5e12a758fbd7491d99d5e3f7e60423ce9d26ed4e4bcaccab3420bf651298901635c849017cf + languageName: node + linkType: hard + "json5@npm:^1.0.2": version: 1.0.2 resolution: "json5@npm:1.0.2" @@ -5335,7 +5344,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.20": +"lodash@npm:^4.17.20, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -6130,6 +6139,18 @@ __metadata: languageName: node linkType: hard +"react-resize-detector@npm:^9.1.0": + version: 9.1.0 + resolution: "react-resize-detector@npm:9.1.0" + dependencies: + lodash: "npm:^4.17.21" + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: ebd45b54ce68368a8d8b6b32ff3b7949e8f56d5cce21fb18b5044612b16ae9adfdd2eefa6494a700aaf6c64b2cec4c3c33d86c464d1772042dbb4d87dd3dafc6 + languageName: node + linkType: hard + "react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5" -- cgit v1.2.3-54-g00ecf