From c4757c72887d0d83a51099c9f33d3e027f55a4e6 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Tue, 15 Aug 2023 12:35:06 +0200 Subject: refactor(language): invalid multiplicity trace Also simplifies attributes and flags for now, as we don't translate them to abstractions. --- .../java/tools/refinery/language/Problem.xtext | 48 ++-- .../resource/ProblemDerivedStateComputer.java | 96 ++++++-- .../resource/ProblemLocationInFileProvider.java | 1 - .../ProblemResourceDescriptionStrategy.java | 44 ++-- .../refinery/language/utils/BuiltinSymbols.java | 6 +- .../refinery/language/utils/CollectedSymbols.java | 15 -- .../refinery/language/utils/ContainmentRole.java | 22 -- .../tools/refinery/language/utils/NodeInfo.java | 9 - .../refinery/language/utils/ProblemDesugarer.java | 25 +- .../tools/refinery/language/utils/ProblemUtil.java | 38 ++- .../refinery/language/utils/RelationInfo.java | 29 --- .../refinery/language/utils/SymbolCollector.java | 255 --------------------- .../tools/refinery/language/builtin.problem | 41 +--- 13 files changed, 164 insertions(+), 465 deletions(-) delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/utils/CollectedSymbols.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/utils/ContainmentRole.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/utils/NodeInfo.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/utils/RelationInfo.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/utils/SymbolCollector.java (limited to 'subprojects/language/src/main') diff --git a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext index 9e330347..0a91178b 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext +++ b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext @@ -14,7 +14,7 @@ Problem: Statement: Assertion | ClassDeclaration | EnumDeclaration | - PredicateDefinition | FunctionDefinition | /* RuleDefinition | */ + PredicateDefinition | /* FunctionDefinition | RuleDefinition | */ ScopeDeclaration | IndividualDeclaration; ClassDeclaration: @@ -32,7 +32,7 @@ EnumLiteral returns Node: name=Identifier; FeatureDeclaration: - ReferenceDeclaration | AttributeDeclaration | FlagDeclaration; + ReferenceDeclaration /* | AttributeDeclaration | FlagDeclaration */; enum ReferenceKind: REFERENCE="refers" | CONTAINMENT="contains" | CONTAINER="container"; @@ -44,23 +44,17 @@ ReferenceDeclaration: name=Identifier ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?; -enum PrimitiveType: - INT="int" | REAL="real" | STRING="string"; - -AttributeDeclaration: - attributeType=PrimitiveType name=Identifier; - -FlagDeclaration: - "bool" name=Identifier; - -enum ErrorKind returns PredicateKind: - ERROR="error"; - -enum PredicateKind: - ERROR="error" | CONTAINED="contained" | CONTAINMENT="containment"; +//enum PrimitiveType: +// INT="int" | REAL="real" | STRING="string"; +// +//AttributeDeclaration: +// attributeType=PrimitiveType name=Identifier; +// +//FlagDeclaration: +// "bool" name=Identifier; PredicateDefinition: - (kind=ErrorKind | kind=PredicateKind? "pred") + ("pred" | error?="error" "pred"?) name=Identifier "(" (parameters+=Parameter ("," parameters+=Parameter)*)? ")" ("<->" bodies+=Conjunction (";" bodies+=Conjunction)*)? @@ -69,14 +63,14 @@ PredicateDefinition: Conjunction: literals+=Expr ("," literals+=Expr)*; -FunctionDefinition: - "fn" functionType=PrimitiveType name=Identifier - "(" (parameters+=Parameter ("," parameters+=Parameter)*)? ")" - ("=" cases+=Case (";" cases+=Case)*)? - "."; - -Case: - Conjunction ({Match.condition=current} "->" value=Expr)?; +//FunctionDefinition: +// "fn" functionType=PrimitiveType name=Identifier +// "(" (parameters+=Parameter ("," parameters+=Parameter)*)? ")" +// ("=" cases+=Case (";" cases+=Case)*)? +// "."; +// +//Case: +// Conjunction ({Match.condition=current} "->" value=Expr)?; //RuleDefinition: // "rule" @@ -87,7 +81,7 @@ Case: // "."; Parameter: - (modality=Modality? parameterType=[Relation|QualifiedName])? name=Identifier; + parameterType=[Relation|QualifiedName]? name=Identifier; //Consequent: // actions+=Action ("," actions+=Action)*; @@ -268,7 +262,7 @@ NonContainmentQualifiedName hidden(): NonContainmentIdentifier ("::" Identifier)*; Identifier: - NonContainmentIdentifier | "contains"; + NonContainmentIdentifier | "contains" | "container"; NonContainmentIdentifier: ID | "contained" | "sum" | "prod" | "min" | "max"; diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java index b145ef27..31eb55a6 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java @@ -18,6 +18,7 @@ import org.eclipse.xtext.resource.DerivedStateAwareResource; import org.eclipse.xtext.resource.IDerivedStateComputer; import org.eclipse.xtext.resource.XtextResource; import tools.refinery.language.model.problem.*; +import tools.refinery.language.utils.ProblemUtil; import java.util.*; import java.util.function.Function; @@ -58,7 +59,7 @@ public class ProblemDerivedStateComputer implements IDerivedStateComputer { } protected void installDerivedProblemState(Problem problem, Adapter adapter, boolean preLinkingPhase) { - installNewNodes(problem, adapter); + installDerivedClassDeclarationState(problem, adapter); if (preLinkingPhase) { return; } @@ -66,24 +67,67 @@ public class ProblemDerivedStateComputer implements IDerivedStateComputer { derivedVariableComputer.installDerivedVariables(problem, nodeNames); } - protected void installNewNodes(Problem problem, Adapter adapter) { - for (Statement statement : problem.getStatements()) { - if (statement instanceof ClassDeclaration declaration && !declaration.isAbstract() - && declaration.getNewNode() == null) { + protected void installDerivedClassDeclarationState(Problem problem, Adapter adapter) { + for (var statement : problem.getStatements()) { + if (statement instanceof ClassDeclaration classDeclaration) { + installOrRemoveNewNode(adapter, classDeclaration); + for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { + if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) { + installOrRemoveInvalidMultiplicityPredicate(adapter, classDeclaration, referenceDeclaration); + } + } + } + } + } + + protected void installOrRemoveNewNode(Adapter adapter, ClassDeclaration declaration) { + if (declaration.isAbstract()) { + var newNode = declaration.getNewNode(); + if (newNode != null) { + declaration.setNewNode(null); + adapter.removeNewNode(declaration); + } + } else { + if (declaration.getNewNode() == null) { var newNode = adapter.createNewNodeIfAbsent(declaration, key -> createNode(NEW_NODE)); declaration.setNewNode(newNode); } } } + protected void installOrRemoveInvalidMultiplicityPredicate( + Adapter adapter, ClassDeclaration containingClassDeclaration, ReferenceDeclaration declaration) { + if (ProblemUtil.hasMultiplicityConstraint(declaration)) { + if (declaration.getInvalidMultiplicity() == null) { + var invalidMultiplicity = adapter.createInvalidMultiplicityPredicateIfAbsent(declaration, key -> { + var predicate = ProblemFactory.eINSTANCE.createPredicateDefinition(); + predicate.setError(true); + predicate.setName("invalidMultiplicity"); + var parameter = ProblemFactory.eINSTANCE.createParameter(); + parameter.setParameterType(containingClassDeclaration); + parameter.setName("node"); + predicate.getParameters().add(parameter); + return predicate; + }); + declaration.setInvalidMultiplicity(invalidMultiplicity); + } + } else { + var invalidMultiplicity = declaration.getInvalidMultiplicity(); + if (invalidMultiplicity != null) { + declaration.setInvalidMultiplicity(null); + adapter.removeInvalidMultiplicityPredicate(declaration); + } + } + } + protected Set installDerivedNodes(Problem problem) { var collector = nodeNameCollectorProvider.get(); collector.collectNodeNames(problem); Set nodeNames = collector.getNodeNames(); - List grapNodes = problem.getNodes(); + List graphNodes = problem.getNodes(); for (String nodeName : nodeNames) { var graphNode = createNode(nodeName); - grapNodes.add(graphNode); + graphNodes.add(graphNode); } return nodeNames; } @@ -104,15 +148,24 @@ public class ProblemDerivedStateComputer implements IDerivedStateComputer { } protected void discardDerivedProblemState(Problem problem, Adapter adapter) { - Set classDeclarations = new HashSet<>(); + var abstractClassDeclarations = new HashSet(); + var referenceDeclarationsWithMultiplicity = new HashSet(); problem.getNodes().clear(); for (var statement : problem.getStatements()) { if (statement instanceof ClassDeclaration classDeclaration) { classDeclaration.setNewNode(null); - classDeclarations.add(classDeclaration); + if (classDeclaration.isAbstract()) { + abstractClassDeclarations.add(classDeclaration); + } + for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { + if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration && + ProblemUtil.hasMultiplicityConstraint(referenceDeclaration)) { + referenceDeclarationsWithMultiplicity.add(referenceDeclaration); + } + } } } - adapter.retainAll(classDeclarations); + adapter.retainAll(abstractClassDeclarations, referenceDeclarationsWithMultiplicity); derivedVariableComputer.discardDerivedVariables(problem); } @@ -134,14 +187,31 @@ public class ProblemDerivedStateComputer implements IDerivedStateComputer { protected static class Adapter extends AdapterImpl { private final Map newNodes = new HashMap<>(); + private final Map invalidMultiplicityPredicates = new HashMap<>(); public Node createNewNodeIfAbsent(ClassDeclaration classDeclaration, - Function createNode) { + Function createNode) { return newNodes.computeIfAbsent(classDeclaration, createNode); } - public void retainAll(Collection classDeclarations) { - newNodes.keySet().retainAll(classDeclarations); + public void removeNewNode(ClassDeclaration classDeclaration) { + newNodes.remove(classDeclaration); + } + + public PredicateDefinition createInvalidMultiplicityPredicateIfAbsent( + ReferenceDeclaration referenceDeclaration, + Function createPredicate) { + return invalidMultiplicityPredicates.computeIfAbsent(referenceDeclaration, createPredicate); + } + + public void removeInvalidMultiplicityPredicate(ReferenceDeclaration referenceDeclaration) { + invalidMultiplicityPredicates.remove(referenceDeclaration); + } + + public void retainAll(Collection abstractClassDeclarations, + Collection referenceDeclarationsWithMultiplicity) { + newNodes.keySet().retainAll(abstractClassDeclarations); + invalidMultiplicityPredicates.keySet().retainAll(referenceDeclarationsWithMultiplicity); } @Override diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java index 1fe2df89..29eaad84 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java @@ -8,7 +8,6 @@ package tools.refinery.language.resource; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.resource.DefaultLocationInFileProvider; import org.eclipse.xtext.util.ITextRegion; - import tools.refinery.language.model.problem.ImplicitVariable; import tools.refinery.language.model.problem.Node; import tools.refinery.language.utils.ProblemUtil; diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java index 630be379..a16f77eb 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java @@ -5,6 +5,9 @@ */ package tools.refinery.language.resource; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Inject; +import com.google.inject.Singleton; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.naming.IQualifiedNameConverter; @@ -13,19 +16,18 @@ import org.eclipse.xtext.resource.EObjectDescription; import org.eclipse.xtext.resource.IEObjectDescription; import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; import org.eclipse.xtext.util.IAcceptor; - -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import tools.refinery.language.model.problem.NamedElement; -import tools.refinery.language.model.problem.Node; -import tools.refinery.language.model.problem.Problem; -import tools.refinery.language.model.problem.Variable; +import tools.refinery.language.model.problem.*; import tools.refinery.language.naming.NamingUtil; import tools.refinery.language.utils.ProblemUtil; +import java.util.Map; + @Singleton public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { + public static final String ERROR_PREDICATE = "tools.refinery.language.resource" + + ".ProblemResourceDescriptionStrategy.ERROR_PREDICATE"; + public static final String ERROR_PREDICATE_TRUE = "true"; + @Inject private IQualifiedNameConverter qualifiedNameConverter; @@ -40,9 +42,10 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti } var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); var problemQualifiedName = getNameAsQualifiedName(problem); + var userData = getUserData(eObject); boolean nameExported; if (shouldExportSimpleName(eObject)) { - acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, acceptor); + acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, userData, acceptor); nameExported = true; } else { nameExported = false; @@ -56,7 +59,7 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti } qualifiedName = parentQualifiedName.append(qualifiedName); if (shouldExportSimpleName(parent)) { - acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, acceptor); + acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, userData, acceptor); nameExported = true; } else { nameExported = false; @@ -64,16 +67,15 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti parent = parent.eContainer(); } if (!nameExported) { - acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, acceptor); + acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, userData, acceptor); } return true; } protected QualifiedName getNameAsQualifiedName(EObject eObject) { - if (!(eObject instanceof NamedElement)) { + if (!(eObject instanceof NamedElement namedElement)) { return null; } - var namedElement = (NamedElement) eObject; var name = namedElement.getName(); if (NamingUtil.isNullOrEmpty(name)) { return null; @@ -93,16 +95,28 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti return true; } + protected Map getUserData(EObject eObject) { + var builder = ImmutableMap.builder(); + if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) { + builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE); + } + return builder.build(); + } + protected boolean shouldExportSimpleName(EObject eObject) { if (eObject instanceof Node node) { return !ProblemUtil.isNewNode(node); } + if (eObject instanceof PredicateDefinition predicateDefinition) { + return !ProblemUtil.isInvalidMultiplicityConstraint(predicateDefinition); + } return true; } private void acceptEObjectDescription(EObject eObject, QualifiedName prefix, QualifiedName qualifiedName, - IAcceptor acceptor) { + Map userData, IAcceptor acceptor) { var qualifiedNameWithPrefix = prefix == null ? qualifiedName : prefix.append(qualifiedName); - acceptor.accept(EObjectDescription.create(qualifiedNameWithPrefix, eObject)); + var description = EObjectDescription.create(qualifiedNameWithPrefix, eObject, userData); + acceptor.accept(description); } } diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/BuiltinSymbols.java b/subprojects/language/src/main/java/tools/refinery/language/utils/BuiltinSymbols.java index c8c7fd4a..70a86b51 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/BuiltinSymbols.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/BuiltinSymbols.java @@ -7,7 +7,7 @@ package tools.refinery.language.utils; import tools.refinery.language.model.problem.*; -public record BuiltinSymbols(Problem problem, ClassDeclaration node, ReferenceDeclaration equals, - PredicateDefinition exists, PredicateDefinition contained, PredicateDefinition contains, - PredicateDefinition root) { +public record BuiltinSymbols(Problem problem, ClassDeclaration node, PredicateDefinition equals, + PredicateDefinition exists, ClassDeclaration contained, PredicateDefinition contains, + PredicateDefinition invalidNumberOfContainers) { } diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/CollectedSymbols.java b/subprojects/language/src/main/java/tools/refinery/language/utils/CollectedSymbols.java deleted file mode 100644 index e4e4d07a..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/CollectedSymbols.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.utils; - -import java.util.Map; - -import tools.refinery.language.model.problem.Node; -import tools.refinery.language.model.problem.Relation; - -public record CollectedSymbols(Map nodes, Map relations) { - -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ContainmentRole.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ContainmentRole.java deleted file mode 100644 index a43c7dfe..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/ContainmentRole.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.utils; - -import tools.refinery.language.model.problem.PredicateKind; - -public enum ContainmentRole { - NONE, - CONTAINED, - CONTAINMENT; - - public static ContainmentRole fromPredicateKind(PredicateKind predicateKind) { - return switch (predicateKind) { - case CONTAINED -> CONTAINED; - case CONTAINMENT -> CONTAINMENT; - default -> NONE; - }; - } -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/NodeInfo.java b/subprojects/language/src/main/java/tools/refinery/language/utils/NodeInfo.java deleted file mode 100644 index 0fa7a454..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/NodeInfo.java +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.utils; - -public record NodeInfo(String name, boolean individual) { -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java index 738a0896..9f08654c 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java @@ -6,7 +6,6 @@ package tools.refinery.language.utils; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; @@ -21,9 +20,6 @@ public class ProblemDesugarer { @Inject private IResourceScopeCache cache = IResourceScopeCache.NullImpl.INSTANCE; - @Inject - private Provider symbolCollectorProvider; - public Optional getBuiltinProblem(EObject context) { return Optional.ofNullable(context).map(EObject::eResource).flatMap(resource -> cache.get("builtinProblem", resource, () -> doGetBuiltinProblem(resource))); @@ -43,12 +39,13 @@ public class ProblemDesugarer { private BuiltinSymbols doGetBuiltinSymbols(Problem builtin) { var node = doGetDeclaration(builtin, ClassDeclaration.class, "node"); - var equals = doGetEqualsReference(node); + var equals = doGetDeclaration(builtin, PredicateDefinition.class, "equals"); var exists = doGetDeclaration(builtin, PredicateDefinition.class, "exists"); - var contained = doGetDeclaration(builtin, PredicateDefinition.class, "contained"); + var contained = doGetDeclaration(builtin, ClassDeclaration.class, "contained"); var contains = doGetDeclaration(builtin, PredicateDefinition.class, "contains"); - var root = doGetDeclaration(builtin, PredicateDefinition.class, "root"); - return new BuiltinSymbols(builtin, node, equals, exists, contained, contains, root); + var invalidNumberOfContainers = doGetDeclaration(builtin, PredicateDefinition.class, + "invalidNumberOfContainers"); + return new BuiltinSymbols(builtin, node, equals, exists, contained, contains, invalidNumberOfContainers); } private T doGetDeclaration(Problem builtin, Class type, String name) { @@ -57,13 +54,6 @@ public class ProblemDesugarer { .orElseThrow(() -> new IllegalArgumentException("Built-in declaration " + name + " was not found")); } - private ReferenceDeclaration doGetEqualsReference(ClassDeclaration nodeClassDeclaration) { - return (ReferenceDeclaration) nodeClassDeclaration.getFeatureDeclarations().stream() - .filter(reference -> reference instanceof ReferenceDeclaration && - "equals".equals(reference.getName())).findFirst() - .orElseThrow(() -> new IllegalArgumentException("Reference " + "equals" + " not found")); - } - public Collection getSuperclassesAndSelf(ClassDeclaration classDeclaration) { return cache.get(Tuples.create(classDeclaration, "superclassesAndSelf"), classDeclaration.eResource(), () -> doGetSuperclassesAndSelf(classDeclaration)); @@ -109,9 +99,4 @@ public class ProblemDesugarer { public boolean isContainmentReference(ReferenceDeclaration referenceDeclaration) { return referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT; } - - public CollectedSymbols collectSymbols(Problem problem) { - return cache.get(Tuples.create(problem, "collectedSymbols"), problem.eResource(), - () -> symbolCollectorProvider.get().collectSymbols(problem)); - } } 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 9486dc2a..bac274b0 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 @@ -8,32 +8,12 @@ package tools.refinery.language.utils; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; -import tools.refinery.language.model.problem.ImplicitVariable; -import tools.refinery.language.model.problem.Node; -import tools.refinery.language.model.problem.ProblemPackage; -import tools.refinery.language.model.problem.Variable; +import tools.refinery.language.model.problem.*; public final class ProblemUtil { public static final String BUILTIN_LIBRARY_NAME = "builtin"; - public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(BUILTIN_LIBRARY_NAME); - public static final String NODE_CLASS_NAME = "node"; - - public static final String DOMAIN_CLASS_NAME = "domain"; - - public static final String DATA_CLASS_NAME = "data"; - - public static final String INT_CLASS_NAME = "int"; - - public static final String REAL_CLASS_NAME = "real"; - - public static final String STRING_CLASS_NAME = "string"; - - public static final String EQUALS_RELATION_NAME = "equals"; - - public static final String EXISTS_PREDICATE_NAME = "exists"; - private ProblemUtil() { throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); } @@ -80,6 +60,22 @@ public final class ProblemUtil { return node.eContainingFeature() == ProblemPackage.Literals.CLASS_DECLARATION__NEW_NODE; } + public static boolean isInvalidMultiplicityConstraint(PredicateDefinition predicateDefinition) { + return predicateDefinition.eContainingFeature() == + ProblemPackage.Literals.REFERENCE_DECLARATION__INVALID_MULTIPLICITY; + } + + public static boolean hasMultiplicityConstraint(ReferenceDeclaration referenceDeclaration) { + var multiplicity = referenceDeclaration.getMultiplicity(); + if (multiplicity instanceof UnboundedMultiplicity) { + return false; + } + if (multiplicity instanceof RangeMultiplicity rangeMultiplicity) { + return rangeMultiplicity.getLowerBound() > 0 || rangeMultiplicity.getUpperBound() >= 0; + } + return true; + } + private static URI getLibraryUri(String libraryName) { return URI.createURI(ProblemUtil.class.getClassLoader() .getResource("tools/refinery/language/%s.problem".formatted(libraryName)).toString()); diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/RelationInfo.java b/subprojects/language/src/main/java/tools/refinery/language/utils/RelationInfo.java deleted file mode 100644 index 1c46fe72..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/RelationInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.utils; - -import tools.refinery.language.model.problem.*; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public record RelationInfo(String name, ContainmentRole containmentRole, List parameters, - Multiplicity multiplicity, Relation opposite, Collection bodies, - Collection assertions, Collection typeScopes) { - public RelationInfo(String name, ContainmentRole containmentRole, List parameters, - Multiplicity multiplicity, Relation opposite, Collection bodies) { - this(name, containmentRole, parameters, multiplicity, opposite, bodies, new ArrayList<>(), new ArrayList<>()); - } - - public boolean hasDefinition() { - return bodies != null && !bodies.isEmpty(); - } - - public int arity() { - return parameters.size(); - } -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/SymbolCollector.java b/subprojects/language/src/main/java/tools/refinery/language/utils/SymbolCollector.java deleted file mode 100644 index a4ea1113..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/SymbolCollector.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.utils; - -import com.google.inject.Inject; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.naming.IQualifiedNameConverter; -import org.eclipse.xtext.naming.IQualifiedNameProvider; -import tools.refinery.language.model.problem.*; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -class SymbolCollector { - @Inject - private IQualifiedNameProvider qualifiedNameProvider; - - @Inject - private IQualifiedNameConverter qualifiedNameConverter; - - @Inject - private ProblemDesugarer desugarer; - - private BuiltinSymbols builtinSymbols; - - private final Map nodes = new LinkedHashMap<>(); - - private final Map relations = new LinkedHashMap<>(); - - public CollectedSymbols collectSymbols(Problem problem) { - builtinSymbols = desugarer.getBuiltinSymbols(problem).orElseThrow(() -> new IllegalArgumentException( - "Problem has no associated built-in library")); - collectOwnSymbols(builtinSymbols.problem()); - collectOwnSymbols(problem); - return new CollectedSymbols(nodes, relations); - } - - public void collectOwnSymbols(Problem problem) { - collectOwnRelations(problem); - collectOwnNodes(problem); - collectOwnAssertions(problem); - } - - private void collectOwnRelations(Problem problem) { - for (var statement : problem.getStatements()) { - if (statement instanceof PredicateDefinition predicateDefinition) { - collectPredicate(predicateDefinition); - } else if (statement instanceof ClassDeclaration classDeclaration) { - collectClass(classDeclaration); - } else if (statement instanceof EnumDeclaration enumDeclaration) { - collectEnum(enumDeclaration); - } else if (statement instanceof RuleDefinition) { - throw new UnsupportedOperationException("Rules are not currently supported"); - } - } - } - - private void collectPredicate(PredicateDefinition predicateDefinition) { - var predicateKind = predicateDefinition.getKind(); - var info = new RelationInfo(getQualifiedNameString(predicateDefinition), - ContainmentRole.fromPredicateKind(predicateKind), predicateDefinition.getParameters(), null, null, - predicateDefinition.getBodies()); - relations.put(predicateDefinition, info); - } - - private void collectClass(ClassDeclaration classDeclaration) { - var contained = classDeclaration != builtinSymbols.node(); - var containmentRole = contained ? ContainmentRole.CONTAINED : ContainmentRole.NONE; - var instanceParameter = ProblemFactory.eINSTANCE.createParameter(); - instanceParameter.setName("instance"); - var classInfo = new RelationInfo(getQualifiedNameString(classDeclaration), containmentRole, - List.of(instanceParameter), null, null, List.of()); - relations.put(classDeclaration, classInfo); - collectFeatures(classDeclaration); - } - - private void collectFeatures(ClassDeclaration classDeclaration) { - for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { - if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) { - collectReference(classDeclaration, referenceDeclaration); - } else if (featureDeclaration instanceof AttributeDeclaration attributeDeclaration) { - collectAttribute(classDeclaration, attributeDeclaration); - } else if (featureDeclaration instanceof FlagDeclaration flagDeclaration) { - collectFlag(classDeclaration, flagDeclaration); - } else { - throw new IllegalArgumentException("Unknown FeatureDeclaration: " + featureDeclaration); - } - } - } - - private void collectReference(ClassDeclaration classDeclaration, ReferenceDeclaration referenceDeclaration) { - var referenceRole = desugarer.isContainmentReference(referenceDeclaration) ? - ContainmentRole.CONTAINMENT : - ContainmentRole.NONE; - var sourceParameter = ProblemFactory.eINSTANCE.createParameter(); - sourceParameter.setName("source"); - sourceParameter.setParameterType(classDeclaration); - var targetParameter = ProblemFactory.eINSTANCE.createParameter(); - targetParameter.setName("target"); - var multiplicity = referenceDeclaration.getMultiplicity(); - if (multiplicity == null) { - var exactMultiplicity = ProblemFactory.eINSTANCE.createExactMultiplicity(); - exactMultiplicity.setExactValue(1); - multiplicity = exactMultiplicity; - } - targetParameter.setParameterType(referenceDeclaration.getReferenceType()); - var referenceInfo = new RelationInfo(getQualifiedNameString(referenceDeclaration), referenceRole, - List.of(sourceParameter, targetParameter), multiplicity, referenceDeclaration.getOpposite(), - List.of()); - this.relations.put(referenceDeclaration, referenceInfo); - } - - private void collectAttribute(ClassDeclaration classDeclaration, AttributeDeclaration attributeDeclaration) { - // TODO Implement attribute handling. - } - - private void collectFlag(ClassDeclaration classDeclaration, FlagDeclaration flagDeclaration) { - var parameter = ProblemFactory.eINSTANCE.createParameter(); - parameter.setName("object"); - parameter.setParameterType(classDeclaration); - var referenceInfo = new RelationInfo(getQualifiedNameString(flagDeclaration), ContainmentRole.NONE, - List.of(parameter), null, null, List.of()); - this.relations.put(flagDeclaration, referenceInfo); - } - - private void collectEnum(EnumDeclaration enumDeclaration) { - var instanceParameter = ProblemFactory.eINSTANCE.createParameter(); - instanceParameter.setName("instance"); - var info = new RelationInfo(getQualifiedNameString(enumDeclaration), ContainmentRole.NONE, - List.of(instanceParameter), null, null, List.of()); - this.relations.put(enumDeclaration, info); - } - - private void collectOwnNodes(Problem problem) { - for (var statement : problem.getStatements()) { - if (statement instanceof IndividualDeclaration individualDeclaration) { - collectIndividuals(individualDeclaration); - } else if (statement instanceof ClassDeclaration classDeclaration) { - collectNewNode(classDeclaration); - } else if (statement instanceof EnumDeclaration enumDeclaration) { - collectEnumLiterals(enumDeclaration); - } - } - for (var node : problem.getNodes()) { - addNode(node, false); - } - } - - private void collectIndividuals(IndividualDeclaration individualDeclaration) { - for (var individual : individualDeclaration.getNodes()) { - addNode(individual, true); - } - } - - private void collectNewNode(ClassDeclaration classDeclaration) { - var newNode = classDeclaration.getNewNode(); - if (newNode != null) { - addNode(newNode, false); - } - } - - private void collectEnumLiterals(EnumDeclaration enumDeclaration) { - for (var literal : enumDeclaration.getLiterals()) { - addNode(literal, true); - } - } - - private void addNode(Node node, boolean individual) { - var info = new NodeInfo(getQualifiedNameString(node), individual); - this.nodes.put(node, info); - } - - private String getQualifiedNameString(EObject eObject) { - var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(eObject); - if (qualifiedName == null) { - return null; - } - return qualifiedNameConverter.toString(qualifiedName); - } - - private void collectOwnAssertions(Problem problem) { - for (var statement : problem.getStatements()) { - if (statement instanceof Assertion assertion) { - collectAssertion(assertion); - } else if (statement instanceof PredicateDefinition predicateDefinition) { - collectPredicateAssertion(predicateDefinition); - } else if (statement instanceof ClassDeclaration classDeclaration) { - collectClassAssertion(classDeclaration); - } else if (statement instanceof EnumDeclaration enumDeclaration) { - collectEnumAssertions(enumDeclaration); - } - } - } - - private void collectAssertion(Assertion assertion) { - var relationInfo = this.relations.get(assertion.getRelation()); - if (relationInfo == null) { - throw new IllegalStateException("Assertion refers to unknown relation"); - } - if (assertion.getArguments().size() != relationInfo.parameters().size()) { - // Silently ignoring assertions of invalid arity helps when SymbolCollector is called on an invalid - // Problem during editing. The errors can still be detected by the Problem validator. - return; - } - relationInfo.assertions().add(assertion); - } - - private void collectPredicateAssertion(PredicateDefinition predicateDefinition) { - if (predicateDefinition.getKind() != PredicateKind.ERROR) { - return; - } - int arity = predicateDefinition.getParameters().size(); - addAssertion(predicateDefinition, LogicValue.FALSE, new Node[arity]); - } - - private void collectClassAssertion(ClassDeclaration classDeclaration) { - var node = classDeclaration.getNewNode(); - if (node == null) { - return; - } - addAssertion(classDeclaration, LogicValue.TRUE, node); - addAssertion(builtinSymbols.exists(), LogicValue.UNKNOWN, node); - addAssertion(builtinSymbols.equals(), LogicValue.UNKNOWN, node, node); - } - - private void collectEnumAssertions(EnumDeclaration enumDeclaration) { - for (var literal : enumDeclaration.getLiterals()) { - addAssertion(enumDeclaration, LogicValue.TRUE, literal); - } - } - - private void addAssertion(Relation relation, LogicValue logicValue, Node... nodes) { - var assertion = ProblemFactory.eINSTANCE.createAssertion(); - assertion.setRelation(relation); - for (var node : nodes) { - AssertionArgument argument; - if (node == null) { - argument = ProblemFactory.eINSTANCE.createWildcardAssertionArgument(); - } else { - var nodeArgument = ProblemFactory.eINSTANCE.createNodeAssertionArgument(); - nodeArgument.setNode(node); - argument = nodeArgument; - } - assertion.getArguments().add(argument); - } - var value = ProblemFactory.eINSTANCE.createLogicConstant(); - value.setLogicValue(logicValue); - assertion.setValue(value); - collectAssertion(assertion); - } -} diff --git a/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem b/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem index 9c1d7669..8570e363 100644 --- a/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem +++ b/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem @@ -3,43 +3,14 @@ % SPDX-License-Identifier: EPL-2.0 problem builtin. -abstract class node { - refers node[] equals opposite equals -} +abstract class node. -pred exists(node node). +pred exists(node). -% class Integer { -% int intValue -% } -% -% class Real { -% real realValue -% } -% -% class String { -% string stringValue -% } -% -% enum Boolean { -% TRUE, FALSE -% } +pred equals(left, right). -pred contained(node node). +abstract class contained extends node. -pred contains(node container, node contained). +pred contains(container, contained contained). -pred root(node node). - -% error missingContainer(contained node) <-> -% !contains(node, _), !root(node). -% -% error tooManyContainers(contained node) <-> -% #contains(_, node) > 1 -% ; -% contains(_, node), root(node) -% ; -% contains(_, node), !contained(node). -% -% error containmentCycle(node node) <-> -% contains+(node, node). +error invalidNumberOfContainers(contained contained). -- cgit v1.2.3-54-g00ecf 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 (limited to 'subprojects/language/src/main') 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 From a49083f31679c47e1685e0cedbc9a40cc8f48fd8 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sat, 26 Aug 2023 21:44:58 +0200 Subject: refactor(frontent): improve graph drawing --- .../patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch | 13 + gradle/libs.versions.toml | 1 + subprojects/frontend/index.html | 1 + subprojects/frontend/src/editor/EditorStore.ts | 7 +- subprojects/frontend/src/editor/EditorTheme.ts | 4 +- .../frontend/src/graph/DotGraphVisualizer.tsx | 86 ++---- subprojects/frontend/src/graph/GraphStore.ts | 51 ++++ subprojects/frontend/src/graph/GraphTheme.tsx | 76 ++++- subprojects/frontend/src/graph/ZoomCanvas.tsx | 5 +- subprojects/frontend/src/graph/dotSource.ts | 309 +++++++++++++++++++++ subprojects/frontend/src/graph/postProcessSVG.ts | 133 ++++++++- subprojects/frontend/src/utils/svgURL.ts | 9 + .../frontend/src/xtext/xtextServiceResults.ts | 30 +- subprojects/frontend/vite.config.ts | 2 +- .../ProblemCrossrefProposalProvider.java | 5 +- .../ProblemSemanticHighlightingCalculator.java | 2 +- .../language/semantics/metadata/BuiltInDetail.java | 10 + .../language/semantics/metadata/ClassDetail.java | 16 ++ .../language/semantics/metadata/Metadata.java | 2 +- .../semantics/metadata/MetadataCreator.java | 181 ++++++++++++ .../language/semantics/metadata/NodeKind.java | 2 +- .../language/semantics/metadata/NodeMetadata.java | 2 +- .../metadata/OppositeReferenceDetail.java | 9 + .../semantics/metadata/PredicateDetail.java | 16 ++ .../semantics/metadata/ReferenceDetail.java | 16 ++ .../semantics/metadata/RelationDetail.java | 10 + .../language/semantics/metadata/RelationKind.java | 18 -- .../semantics/metadata/RelationMetadata.java | 3 +- .../language/semantics/model/ModelInitializer.java | 10 +- subprojects/language-web/build.gradle.kts | 1 + .../language/web/ProblemWebSocketServlet.java | 4 +- .../language/web/semantics/SemanticsService.java | 2 +- .../web/semantics/SemanticsSuccessResult.java | 5 +- .../language/web/semantics/SemanticsWorker.java | 24 +- .../web/xtext/server/message/XtextWebRequest.java | 9 +- .../xtext/servlet/RuntimeTypeAdapterFactory.java | 304 ++++++++++++++++++++ .../language/web/xtext/servlet/XtextWebSocket.java | 12 +- .../tools/refinery/language/utils/ProblemUtil.java | 5 +- yarn.lock | 4 +- 39 files changed, 1252 insertions(+), 147 deletions(-) create mode 100644 subprojects/frontend/src/graph/GraphStore.ts create mode 100644 subprojects/frontend/src/graph/dotSource.ts create mode 100644 subprojects/frontend/src/utils/svgURL.ts create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/BuiltInDetail.java create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ClassDetail.java create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/OppositeReferenceDetail.java create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/PredicateDetail.java create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ReferenceDetail.java create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationDetail.java delete mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationKind.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java (limited to 'subprojects/language/src/main') diff --git a/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch b/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch index 161db0d7..0a4110c5 100644 --- a/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch +++ b/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch @@ -49,6 +49,19 @@ index 96ae02b6edd947ac9086f3108986c08d91470cba..c4422b08d73f7fe73dc52ad905cf981d var data = extractAllElementsData(newSvg); this._dispatch.call('dataExtractEnd', this); postProcessDataPass1Local(data); +diff --git a/src/element.js b/src/element.js +index 5aa398a6cf2550e15f642aea4eaa5a1c69af69ad..5d799e38566e8f847aa1ba80f4c575911e9851cf 100644 +--- a/src/element.js ++++ b/src/element.js +@@ -108,6 +108,8 @@ export function createElement(data) { + return document.createTextNode(""); + } else if (data.tag == '#comment') { + return document.createComment(data.comment); ++ } else if (data.tag == 'div' || data.tag == 'DIV') { ++ return document.createElement('div'); + } else { + return document.createElementNS('http://www.w3.org/2000/svg', data.tag); + } diff --git a/src/graphviz.js b/src/graphviz.js index c4638cb0e4042844c59c52dfe4749e13999fef6e..28dcfb71ad787c78645c460a29e9c52295c5f6bf 100644 --- a/src/graphviz.js diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 637e68c6..45d3b35f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ ecore-codegen = { group = "org.eclipse.emf", name = "org.eclipse.emf.codegen.eco gradlePlugin-frontend = { group = "org.siouan", name = "frontend-gradle-plugin-jdk11", version = "6.0.0" } gradlePlugin-shadow = { group = "com.github.johnrengelman", name = "shadow", version = "8.1.1" } gradlePlugin-sonarqube = { group = "org.sonarsource.scanner.gradle", name = "sonarqube-gradle-plugin", version = "4.3.0.3225" } +gson = { group = "com.google.code.gson", name = "gson", version = "2.10.1" } hamcrest = { group = "org.hamcrest", name = "hamcrest", version = "2.2" } jetty-server = { group = "org.eclipse.jetty", name = "jetty-server", version.ref = "jetty" } jetty-servlet = { group = "org.eclipse.jetty.ee10", name = "jetty-ee10-servlet", version.ref = "jetty" } diff --git a/subprojects/frontend/index.html b/subprojects/frontend/index.html index f4b46da2..8992d538 100644 --- a/subprojects/frontend/index.html +++ b/subprojects/frontend/index.html @@ -19,6 +19,7 @@ diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index 10f01099..b5989ad1 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts @@ -26,6 +26,7 @@ import { makeAutoObservable, observable, runInAction } from 'mobx'; import { nanoid } from 'nanoid'; import type PWAStore from '../PWAStore'; +import GraphStore from '../graph/GraphStore'; import getLogger from '../utils/getLogger'; import type XtextClient from '../xtext/XtextClient'; import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; @@ -66,7 +67,7 @@ export default class EditorStore { semanticsError: string | undefined; - semantics: SemanticsSuccessResult | undefined; + graph: GraphStore; constructor(initialValue: string, pwaStore: PWAStore) { this.id = nanoid(); @@ -86,12 +87,12 @@ export default class EditorStore { })().catch((error) => { log.error('Failed to load XtextClient', error); }); + this.graph = new GraphStore(); makeAutoObservable(this, { id: false, state: observable.ref, client: observable.ref, view: observable.ref, - semantics: observable.ref, searchPanel: false, lintPanel: false, contentAssist: false, @@ -298,7 +299,7 @@ export default class EditorStore { setSemantics(semantics: SemanticsSuccessResult) { this.semanticsError = undefined; - this.semantics = semantics; + this.graph.setSemantics(semantics); } dispose(): void { diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index 4508273b..308d5be0 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts @@ -10,9 +10,7 @@ import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; import { alpha, styled, type CSSObject } from '@mui/material/styles'; -function svgURL(svg: string): string { - return `url('data:image/svg+xml;utf8,${svg}')`; -} +import svgURL from '../utils/svgURL'; export default styled('div', { name: 'EditorTheme', diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx index 29e750f5..291314ec 100644 --- a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx +++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx @@ -8,76 +8,24 @@ import * as d3 from 'd3'; import { type Graphviz, graphviz } from 'd3-graphviz'; import type { BaseType, Selection } from 'd3-selection'; import { reaction, type IReactionDisposer } from 'mobx'; +import { observer } from 'mobx-react-lite'; import { useCallback, useRef } from 'react'; import { useRootStore } from '../RootStoreProvider'; -import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; +import getLogger from '../utils/getLogger'; import GraphTheme from './GraphTheme'; import { FitZoomCallback } from './ZoomCanvas'; +import dotSource from './dotSource'; import postProcessSvg from './postProcessSVG'; -function toGraphviz( - semantics: SemanticsSuccessResult | undefined, -): string | undefined { - if (semantics === undefined) { - return undefined; - } - const lines = [ - 'digraph {', - 'graph [bgcolor=transparent];', - `node [fontsize=12, shape=plain, fontname="OpenSans"];`, - 'edge [fontsize=10.5, color=black, fontname="OpenSans"];', - ]; - const nodeIds = semantics.nodes.map((name, i) => name ?? `n${i}`); - lines.push( - ...nodeIds.map( - (id, i) => - `n${i} [id="${id}", label=<
${id}
node
>];`, - ), - ); - Object.keys(semantics.partialInterpretation).forEach((relation) => { - if (relation === 'builtin::equals' || relation === 'builtin::contains') { - return; - } - const tuples = semantics.partialInterpretation[relation]; - if (tuples === undefined) { - return; - } - const first = tuples[0]; - if (first === undefined || first.length !== 3) { - return; - } - const nameFragments = relation.split('::'); - const simpleName = nameFragments[nameFragments.length - 1] ?? relation; - lines.push( - ...tuples.map(([from, to, value]) => { - if ( - typeof from !== 'number' || - typeof to !== 'number' || - typeof value !== 'string' - ) { - return ''; - } - const isUnknown = value === 'UNKNOWN'; - return `n${from} -> n${to} [ - id="${nodeIds[from]},${nodeIds[to]},${relation}", - xlabel="${simpleName}", - style="${isUnknown ? 'dashed' : 'solid'}", - class="edge-${value}" - ];`; - }), - ); - }); - lines.push('}'); - return lines.join('\n'); -} +const LOG = getLogger('graph.DotGraphVisualizer'); function ptToPx(pt: number): number { return (pt * 4) / 3; } -export default function DotGraphVisualizer({ +function DotGraphVisualizer({ fitZoom, transitionTime, }: { @@ -88,6 +36,7 @@ export default function DotGraphVisualizer({ transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime; const { editorStore } = useRootStore(); + const graph = editorStore?.graph; const disposerRef = useRef(); const graphvizRef = useRef< Graphviz | undefined @@ -113,6 +62,9 @@ export default function DotGraphVisualizer({ undefined >; renderer.keyMode('id'); + ['TRUE', 'UNKNOWN', 'ERROR'].forEach((icon) => + renderer.addImage(`#${icon}`, 16, 16), + ); renderer.zoom(false); renderer.tweenPrecision('5%'); renderer.tweenShapes(false); @@ -125,6 +77,7 @@ export default function DotGraphVisualizer({ */ renderer.transition(transition as any); let newViewBox = { width: 0, height: 0 }; + renderer.onerror(LOG.error.bind(LOG)); renderer.on( 'postProcessSVG', // @ts-expect-error Custom `d3-graphviz` hook not covered by typings. @@ -139,19 +92,24 @@ export default function DotGraphVisualizer({ height: ptToPx(svg.viewBox.baseVal.height), }; } else { + // Do not trigger fit zoom. newViewBox = { width: 0, height: 0 }; } }, ); + renderer.on('renderEnd', () => { + // `d3-graphviz` uses `` elements for traceability, + // so we only remove them after the rendering is finished. + d3.select(element).selectAll('title').remove(); + }); if (fitZoom !== undefined) { renderer.on('transitionStart', () => fitZoom(newViewBox)); } disposerRef.current = reaction( - () => editorStore?.semantics, - (semantics) => { - const str = toGraphviz(semantics); - if (str !== undefined) { - renderer.renderDot(str); + () => dotSource(graph), + (source) => { + if (source !== undefined) { + renderer.renderDot(source); } }, { fireImmediately: true }, @@ -159,7 +117,7 @@ export default function DotGraphVisualizer({ graphvizRef.current = renderer; } }, - [editorStore, fitZoom, transitionTimeOrDefault], + [graph, fitZoom, transitionTimeOrDefault], ); return <GraphTheme ref={setElement} />; @@ -169,3 +127,5 @@ DotGraphVisualizer.defaultProps = { fitZoom: undefined, transitionTime: 250, }; + +export default observer(DotGraphVisualizer); diff --git a/subprojects/frontend/src/graph/GraphStore.ts b/subprojects/frontend/src/graph/GraphStore.ts new file mode 100644 index 00000000..b59bfb7d --- /dev/null +++ b/subprojects/frontend/src/graph/GraphStore.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { makeAutoObservable, observable } from 'mobx'; + +import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; + +export type Visibility = 'all' | 'must' | 'none'; + +export default class GraphStore { + semantics: SemanticsSuccessResult = { + nodes: [], + relations: [], + partialInterpretation: {}, + }; + + visibility = new Map<string, Visibility>(); + + constructor() { + makeAutoObservable(this, { + semantics: observable.ref, + }); + } + + getVisiblity(relation: string): Visibility { + return this.visibility.get(relation) ?? 'none'; + } + + setSemantics(semantics: SemanticsSuccessResult) { + this.semantics = semantics; + this.visibility.clear(); + const names = new Set<string>(); + this.semantics.relations.forEach(({ name, detail }) => { + names.add(name); + if (!this.visibility.has(name)) { + const newVisibility = detail.type === 'builtin' ? 'none' : 'all'; + this.visibility.set(name, newVisibility); + } + }); + const oldNames = new Set<string>(); + this.visibility.forEach((_, key) => oldNames.add(key)); + oldNames.forEach((key) => { + if (!names.has(key)) { + this.visibility.delete(key); + } + }); + } +} diff --git a/subprojects/frontend/src/graph/GraphTheme.tsx b/subprojects/frontend/src/graph/GraphTheme.tsx index 41ba6ba5..989bd0c2 100644 --- a/subprojects/frontend/src/graph/GraphTheme.tsx +++ b/subprojects/frontend/src/graph/GraphTheme.tsx @@ -4,19 +4,28 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { styled, type CSSObject } from '@mui/material/styles'; +import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; +import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw'; +import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; +import { alpha, styled, type CSSObject } from '@mui/material/styles'; -function createEdgeColor(suffix: string, color: string): CSSObject { +import svgURL from '../utils/svgURL'; + +function createEdgeColor( + suffix: string, + stroke: string, + fill?: string, +): CSSObject { return { - [`& .edge-${suffix}`]: { + [`.edge-${suffix}`]: { '& text': { - fill: color, + fill: stroke, }, '& [stroke="black"]': { - stroke: color, + stroke, }, '& [fill="black"]': { - fill: color, + fill: fill ?? stroke, }, }, }; @@ -27,7 +36,7 @@ export default styled('div', { })(({ theme }) => ({ '& svg': { userSelect: 'none', - '& .node': { + '.node': { '& text': { fontFamily: theme.typography.fontFamily, fill: theme.palette.text.primary, @@ -43,10 +52,32 @@ export default styled('div', { }, '& [fill="white"]': { fill: theme.palette.background.default, - stroke: theme.palette.background.default, }, }, - '& .edge': { + '.node-INDIVIDUAL': { + '& [stroke="black"]': { + strokeWidth: 2, + }, + }, + '.node-shadow[fill="white"]': { + fill: alpha( + theme.palette.text.primary, + theme.palette.mode === 'dark' ? 0.32 : 0.24, + ), + }, + '.node-exists-UNKNOWN [stroke="black"]': { + strokeDasharray: '5 2', + }, + '.node-exists-FALSE': { + '& [fill="green"]': { + fill: theme.palette.background.default, + }, + '& [stroke="black"]': { + strokeDasharray: '1 3', + stroke: theme.palette.text.secondary, + }, + }, + '.edge': { '& text': { fontFamily: theme.typography.fontFamily, fill: theme.palette.text.primary, @@ -58,7 +89,32 @@ export default styled('div', { fill: theme.palette.text.primary, }, }, - ...createEdgeColor('UNKNOWN', theme.palette.text.secondary), + ...createEdgeColor('UNKNOWN', theme.palette.text.secondary, 'none'), ...createEdgeColor('ERROR', theme.palette.error.main), + '.icon': { + maskSize: '12px 12px', + maskPosition: '50% 50%', + maskRepeat: 'no-repeat', + width: '100%', + height: '100%', + }, + '.icon-TRUE': { + maskImage: svgURL(labelSVG), + background: theme.palette.text.primary, + }, + '.icon-UNKNOWN': { + maskImage: svgURL(labelOutlinedSVG), + background: theme.palette.text.secondary, + }, + '.icon-ERROR': { + maskImage: svgURL(cancelSVG), + background: theme.palette.error.main, + }, + 'text.label-UNKNOWN': { + fill: theme.palette.text.secondary, + }, + 'text.label-ERROR': { + fill: theme.palette.error.main, + }, }, })); diff --git a/subprojects/frontend/src/graph/ZoomCanvas.tsx b/subprojects/frontend/src/graph/ZoomCanvas.tsx index b8faae27..2bb7f139 100644 --- a/subprojects/frontend/src/graph/ZoomCanvas.tsx +++ b/subprojects/frontend/src/graph/ZoomCanvas.tsx @@ -148,7 +148,8 @@ export default function ZoomCanvas({ const [x, y] = d3.pointer(event, canvas); return [x - width / 2, y - height / 2]; }) - .centroid([0, 0]); + .centroid([0, 0]) + .scaleExtent([1 / 32, 8]); zoomBehavior.on( 'zoom', (event: d3.D3ZoomEvent<HTMLDivElement, unknown>) => { @@ -214,6 +215,6 @@ export default function ZoomCanvas({ ZoomCanvas.defaultProps = { children: undefined, - fitPadding: 16, + fitPadding: 8, transitionTime: 250, }; diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts new file mode 100644 index 00000000..bf45d303 --- /dev/null +++ b/subprojects/frontend/src/graph/dotSource.ts @@ -0,0 +1,309 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { + NodeMetadata, + RelationMetadata, +} from '../xtext/xtextServiceResults'; + +import type GraphStore from './GraphStore'; + +const EDGE_WEIGHT = 1; +const CONTAINMENT_WEIGHT = 5; +const UNKNOWN_WEIGHT_FACTOR = 0.5; + +function nodeName({ simpleName, kind }: NodeMetadata): string { + switch (kind) { + case 'INDIVIDUAL': + return `<b>${simpleName}</b>`; + case 'NEW': + return `<i>${simpleName}</i>`; + default: + return simpleName; + } +} + +function relationName({ simpleName, detail }: RelationMetadata): string { + if (detail.type === 'class' && detail.abstractClass) { + return `<i>${simpleName}</i>`; + } + if (detail.type === 'reference' && detail.containment) { + return `<b>${simpleName}</b>`; + } + return simpleName; +} + +interface NodeData { + exists: string; + equalsSelf: string; + unaryPredicates: Map<RelationMetadata, string>; +} + +function computeNodeData(graph: GraphStore): NodeData[] { + const { + semantics: { nodes, relations, partialInterpretation }, + } = graph; + + const nodeData = Array.from(Array(nodes.length)).map(() => ({ + exists: 'FALSE', + equalsSelf: 'FALSE', + unaryPredicates: new Map(), + })); + + relations.forEach((relation) => { + if (relation.arity !== 1) { + return; + } + const visibility = graph.getVisiblity(relation.name); + if (visibility === 'none') { + return; + } + const interpretation = partialInterpretation[relation.name] ?? []; + interpretation.forEach(([index, value]) => { + if ( + typeof index === 'number' && + typeof value === 'string' && + (visibility === 'all' || value !== 'UNKNOWN') + ) { + nodeData[index]?.unaryPredicates?.set(relation, value); + } + }); + }); + + partialInterpretation['builtin::exists']?.forEach(([index, value]) => { + if (typeof index === 'number' && typeof value === 'string') { + const data = nodeData[index]; + if (data !== undefined) { + data.exists = value; + } + } + }); + + partialInterpretation['builtin::equals']?.forEach(([index, other, value]) => { + if ( + typeof index === 'number' && + index === other && + typeof value === 'string' + ) { + const data = nodeData[index]; + if (data !== undefined) { + data.equalsSelf = value; + } + } + }); + + return nodeData; +} + +function createNodes(graph: GraphStore, lines: string[]): void { + const nodeData = computeNodeData(graph); + const { + semantics: { nodes }, + } = graph; + + nodes.forEach((node, i) => { + const data = nodeData[i]; + if (data === undefined) { + return; + } + const classes = [ + `node-${node.kind} node-exists-${data.exists} node-equalsSelf-${data.equalsSelf}`, + ].join(' '); + const name = nodeName(node); + const border = node.kind === 'INDIVIDUAL' ? 2 : 1; + lines.push(`n${i} [id="${node.name}", class="${classes}", label=< + <table border="${border}" cellborder="0" cellspacing="0" style="rounded" bgcolor="white"> + <tr><td cellpadding="4.5" width="32" bgcolor="green">${name}</td></tr>`); + if (data.unaryPredicates.size > 0) { + lines.push( + '<hr/><tr><td cellpadding="4.5"><table fixedsize="TRUE" align="left" border="0" cellborder="0" cellspacing="0" cellpadding="1.5">', + ); + data.unaryPredicates.forEach((value, relation) => { + lines.push( + `<tr> + <td><img src="#${value}"/></td> + <td width="1.5"></td> + <td align="left" href="#${value}" id="${node.name},${ + relation.name + },label">${relationName(relation)}</td> + </tr>`, + ); + }); + lines.push('</table></td></tr>'); + } + lines.push('</table>>]'); + }); +} + +function compare( + a: readonly (number | string)[], + b: readonly number[], +): number { + if (a.length !== b.length + 1) { + throw new Error('Tuple length mismatch'); + } + for (let i = 0; i < b.length; i += 1) { + const aItem = a[i]; + const bItem = b[i]; + if (typeof aItem !== 'number' || typeof bItem !== 'number') { + throw new Error('Invalid tuple'); + } + if (aItem < bItem) { + return -1; + } + if (aItem > bItem) { + return 1; + } + } + return 0; +} + +function binarySerach( + tuples: readonly (readonly (number | string)[])[], + key: readonly number[], +): string | undefined { + let lower = 0; + let upper = tuples.length - 1; + while (lower <= upper) { + const middle = Math.floor((lower + upper) / 2); + const tuple = tuples[middle]; + if (tuple === undefined) { + throw new Error('Range error'); + } + const result = compare(tuple, key); + if (result === 0) { + const found = tuple[key.length]; + if (typeof found !== 'string') { + throw new Error('Invalid tuple value'); + } + return found; + } + if (result < 0) { + lower = middle + 1; + } else { + // result > 0 + upper = middle - 1; + } + } + return undefined; +} + +function createRelationEdges( + graph: GraphStore, + relation: RelationMetadata, + showUnknown: boolean, + lines: string[], +): void { + const { + semantics: { nodes, partialInterpretation }, + } = graph; + const { detail } = relation; + + let constraint: 'true' | 'false' = 'true'; + let weight = EDGE_WEIGHT; + let penwidth = 1; + let label = `"${relation.simpleName}"`; + if (detail.type === 'reference' && detail.containment) { + weight = CONTAINMENT_WEIGHT; + label = `<<b>${relation.simpleName}</b>>`; + penwidth = 2; + } else if ( + detail.type === 'opposite' && + graph.getVisiblity(detail.opposite) !== 'none' + ) { + constraint = 'false'; + weight = 0; + } + + const tuples = partialInterpretation[relation.name] ?? []; + tuples.forEach(([from, to, value]) => { + const isUnknown = value === 'UNKNOWN'; + if ( + (!showUnknown && isUnknown) || + typeof from !== 'number' || + typeof to !== 'number' || + typeof value !== 'string' + ) { + return; + } + + const fromNode = nodes[from]; + const toNode = nodes[to]; + if (fromNode === undefined || toNode === undefined) { + return; + } + + let dir = 'forward'; + let edgeConstraint = constraint; + let edgeWeight = weight; + const opposite = binarySerach(tuples, [to, from]); + const oppositeUnknown = opposite === 'UNKNOWN'; + const oppositeSet = opposite !== undefined; + const oppositeVisible = oppositeSet && (showUnknown || !oppositeUnknown); + if (opposite === value) { + if (to < from) { + // We already added this edge in the reverse direction. + return; + } + if (to > from) { + dir = 'both'; + } + } else if (oppositeVisible && to < from) { + // Let the opposite edge drive the graph layout. + edgeConstraint = 'false'; + edgeWeight = 0; + } else if (isUnknown && (!oppositeSet || oppositeUnknown)) { + // Only apply the UNKNOWN value penalty if we aren't the opposite + // edge driving the graph layout from above, or the penalty would + // be applied anyway. + edgeWeight *= UNKNOWN_WEIGHT_FACTOR; + } + + lines.push(`n${from} -> n${to} [ + id="${fromNode.name},${toNode.name},${relation.name}", + dir="${dir}", + constraint=${edgeConstraint}, + weight=${edgeWeight}, + xlabel=${label}, + penwidth=${penwidth}, + style="${isUnknown ? 'dashed' : 'solid'}", + class="edge-${value}" + ]`); + }); +} + +function createEdges(graph: GraphStore, lines: string[]): void { + const { + semantics: { relations }, + } = graph; + relations.forEach((relation) => { + if (relation.arity !== 2) { + return; + } + const visibility = graph.getVisiblity(relation.name); + if (visibility !== 'none') { + createRelationEdges(graph, relation, visibility === 'all', lines); + } + }); +} + +export default function dotSource( + graph: GraphStore | undefined, +): string | undefined { + if (graph === undefined) { + return undefined; + } + const lines = [ + 'digraph {', + 'graph [bgcolor=transparent];', + `node [fontsize=12, shape=plain, fontname="OpenSans"];`, + 'edge [fontsize=10.5, color=black, fontname="OpenSans"];', + ]; + createNodes(graph, lines); + createEdges(graph, lines); + lines.push('}'); + return lines.join('\n'); +} diff --git a/subprojects/frontend/src/graph/postProcessSVG.ts b/subprojects/frontend/src/graph/postProcessSVG.ts index 59cc15b9..13e4eb29 100644 --- a/subprojects/frontend/src/graph/postProcessSVG.ts +++ b/subprojects/frontend/src/graph/postProcessSVG.ts @@ -7,19 +7,48 @@ import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; const SVG_NS = 'http://www.w3.org/2000/svg'; +const XLINK_NS = 'http://www.w3.org/1999/xlink'; + +function modifyAttribute(element: Element, attribute: string, change: number) { + const valueString = element.getAttribute(attribute); + if (valueString === null) { + return; + } + const value = parseInt(valueString, 10); + element.setAttribute(attribute, String(value + change)); +} + +function addShadow( + node: SVGGElement, + container: SVGRectElement, + offset: number, +): void { + const shadow = container.cloneNode() as SVGRectElement; + // Leave space for 1pt stroke around the original container. + const offsetWithStroke = offset - 0.5; + modifyAttribute(shadow, 'x', offsetWithStroke); + modifyAttribute(shadow, 'y', offsetWithStroke); + modifyAttribute(shadow, 'width', 1); + modifyAttribute(shadow, 'height', 1); + modifyAttribute(shadow, 'rx', 0.5); + modifyAttribute(shadow, 'ry', 0.5); + shadow.setAttribute('class', 'node-shadow'); + shadow.id = `${node.id},shadow`; + node.insertBefore(shadow, node.firstChild); +} function clipCompartmentBackground(node: SVGGElement) { - // Background rectangle of the node created by the `<table bgcolor="green">` + // Background rectangle of the node created by the `<table bgcolor="white">` // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`. - const container = node.querySelector<SVGRectElement>('rect[fill="green"]'); - // Background rectangle of the lower compartment created by the `<td bgcolor="white">` + const container = node.querySelector<SVGRectElement>('rect[fill="white"]'); + // Background rectangle of the lower compartment created by the `<td bgcolor="green">` // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`. // Since dot doesn't round the coners of `<td>` background, // we have to clip it ourselves. - const compartment = node.querySelector<SVGPolygonElement>( - 'polygon[fill="white"]', - ); - if (container === null || compartment === null) { + const compartment = node.querySelector<SVGRectElement>('rect[fill="green"]'); + // Make sure we provide traceability with IDs also for the border. + const border = node.querySelector<SVGRectElement>('rect[stroke="black"]'); + if (container === null || compartment === null || border === null) { return; } const copyOfContainer = container.cloneNode() as SVGRectElement; @@ -29,6 +58,17 @@ function clipCompartmentBackground(node: SVGGElement) { clipPath.appendChild(copyOfContainer); node.appendChild(clipPath); compartment.setAttribute('clip-path', `url(#${clipId})`); + // Enlarge the compartment to completely cover the background. + modifyAttribute(compartment, 'y', -5); + modifyAttribute(compartment, 'x', -5); + modifyAttribute(compartment, 'width', 10); + modifyAttribute(compartment, 'height', 5); + if (node.classList.contains('node-equalsSelf-UNKNOWN')) { + addShadow(node, container, 6); + } + container.id = `${node.id},container`; + compartment.id = `${node.id},compartment`; + border.id = `${node.id},border`; } function createRect( @@ -51,7 +91,7 @@ function optimizeNodeShapes(node: SVGGElement) { const rect = createRect(bbox, path); rect.setAttribute('rx', '12'); rect.setAttribute('ry', '12'); - node.replaceChild(rect, path); + path.parentNode?.replaceChild(rect, path); }); node.querySelectorAll('polygon').forEach((polygon) => { const bbox = parsePolygonBBox(polygon); @@ -62,18 +102,83 @@ function optimizeNodeShapes(node: SVGGElement) { 'points', `${bbox.x},${bbox.y} ${bbox.x + bbox.width},${bbox.y}`, ); - node.replaceChild(polyline, polygon); + polygon.parentNode?.replaceChild(polyline, polygon); } else { const rect = createRect(bbox, polygon); - node.replaceChild(rect, polygon); + polygon.parentNode?.replaceChild(rect, polygon); } }); clipCompartmentBackground(node); } +function hrefToClass(node: SVGGElement) { + node.querySelectorAll<SVGAElement>('a').forEach((a) => { + if (a.parentNode === null) { + return; + } + const href = a.getAttribute('href') ?? a.getAttributeNS(XLINK_NS, 'href'); + if (href === 'undefined' || !href?.startsWith('#')) { + return; + } + while (a.lastChild !== null) { + const child = a.lastChild; + a.removeChild(child); + if (child.nodeType === Node.ELEMENT_NODE) { + const element = child as Element; + element.classList.add('label', `label-${href.replace('#', '')}`); + a.after(child); + } + } + a.parentNode.removeChild(a); + }); +} + +function replaceImages(node: SVGGElement) { + node.querySelectorAll<SVGImageElement>('image').forEach((image) => { + const href = + image.getAttribute('href') ?? image.getAttributeNS(XLINK_NS, 'href'); + if (href === 'undefined' || !href?.startsWith('#')) { + return; + } + const width = image.getAttribute('width')?.replace('px', '') ?? ''; + const height = image.getAttribute('height')?.replace('px', '') ?? ''; + const foreign = document.createElementNS(SVG_NS, 'foreignObject'); + foreign.setAttribute('x', image.getAttribute('x') ?? ''); + foreign.setAttribute('y', image.getAttribute('y') ?? ''); + foreign.setAttribute('width', width); + foreign.setAttribute('height', height); + const div = document.createElement('div'); + div.classList.add('icon', `icon-${href.replace('#', '')}`); + foreign.appendChild(div); + const sibling = image.nextElementSibling; + // Since dot doesn't respect the `id` attribute on table cells with a single image, + // compute the ID based on the ID of the next element (the label). + if ( + sibling !== null && + sibling.tagName.toLowerCase() === 'g' && + sibling.id !== '' + ) { + foreign.id = `${sibling.id},icon`; + } + image.parentNode?.replaceChild(foreign, image); + }); +} + export default function postProcessSvg(svg: SVGSVGElement) { - svg - .querySelectorAll<SVGTitleElement>('title') - .forEach((title) => title.parentNode?.removeChild(title)); - svg.querySelectorAll<SVGGElement>('g.node').forEach(optimizeNodeShapes); + // svg + // .querySelectorAll<SVGTitleElement>('title') + // .forEach((title) => title.parentElement?.removeChild(title)); + svg.querySelectorAll<SVGGElement>('g.node').forEach((node) => { + optimizeNodeShapes(node); + hrefToClass(node); + replaceImages(node); + }); + // Increase padding to fit box shadows for multi-objects. + const viewBox = [ + svg.viewBox.baseVal.x - 6, + svg.viewBox.baseVal.y - 6, + svg.viewBox.baseVal.width + 12, + svg.viewBox.baseVal.height + 12, + ]; + svg.setAttribute('viewBox', viewBox.join(' ')); } diff --git a/subprojects/frontend/src/utils/svgURL.ts b/subprojects/frontend/src/utils/svgURL.ts new file mode 100644 index 00000000..9b8ecbd5 --- /dev/null +++ b/subprojects/frontend/src/utils/svgURL.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ + +export default function svgURL(svg: string): string { + return `url('data:image/svg+xml;utf8,${svg}')`; +} diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index 12f87b26..caf2cf0b 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts @@ -126,8 +126,36 @@ export const FormattingResult = DocumentStateResult.extend({ export type FormattingResult = z.infer<typeof FormattingResult>; +export const NodeMetadata = z.object({ + name: z.string(), + simpleName: z.string(), + kind: z.enum(['IMPLICIT', 'INDIVIDUAL', 'NEW']), +}); + +export type NodeMetadata = z.infer<typeof NodeMetadata>; + +export const RelationMetadata = z.object({ + name: z.string(), + simpleName: z.string(), + arity: z.number().nonnegative(), + detail: z.union([ + z.object({ type: z.literal('class'), abstractClass: z.boolean() }), + z.object({ type: z.literal('reference'), containment: z.boolean() }), + z.object({ + type: z.literal('opposite'), + container: z.boolean(), + opposite: z.string(), + }), + z.object({ type: z.literal('predicate'), error: z.boolean() }), + z.object({ type: z.literal('builtin') }), + ]), +}); + +export type RelationMetadata = z.infer<typeof RelationMetadata>; + export const SemanticsSuccessResult = z.object({ - nodes: z.string().nullable().array(), + nodes: NodeMetadata.array(), + relations: RelationMetadata.array(), partialInterpretation: z.record( z.string(), z.union([z.number(), z.string()]).array().array(), diff --git a/subprojects/frontend/vite.config.ts b/subprojects/frontend/vite.config.ts index 82e432de..63d5245f 100644 --- a/subprojects/frontend/vite.config.ts +++ b/subprojects/frontend/vite.config.ts @@ -30,7 +30,7 @@ const { mode, isDevelopment, devModePlugins, serverOptions } = process.env['NODE_ENV'] ??= mode; const fontsGlob = [ - 'open-sans-latin-wdth-normal-*.woff2', + 'open-sans-latin-wdth-{normal,italic}-*.woff2', 'jetbrains-mono-latin-wght-{normal,italic}-*.woff2', ]; diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java index ce5e7dad..ea90a82e 100644 --- a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java +++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java @@ -36,7 +36,10 @@ public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider var eObjectDescriptionsByName = new HashMap<QualifiedName, List<IEObjectDescription>>(); for (var candidate : super.queryScope(scope, crossReference, context)) { if (isExistingObject(candidate, crossReference, context)) { - var qualifiedName = candidate.getQualifiedName(); + // {@code getQualifiedName()} will refer to the full name for objects that are loaded from the global + // scope, but {@code getName()} returns the qualified name that we set in + // {@code ProblemResourceDescriptionStrategy}. + var qualifiedName = candidate.getName(); var candidateList = eObjectDescriptionsByName.computeIfAbsent(qualifiedName, ignored -> new ArrayList<>()); candidateList.add(candidate); diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java index 08747ec5..ae8c70e0 100644 --- a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java +++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java @@ -95,7 +95,7 @@ public class ProblemSemanticHighlightingCalculator extends DefaultSemanticHighli } protected String[] getHighlightClass(EObject eObject, EReference reference) { - boolean isError = eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError(); + boolean isError = ProblemUtil.isError(eObject); if (ProblemUtil.isBuiltIn(eObject)) { var className = isError ? ERROR_CLASS : BUILTIN_CLASS; return new String[] { className }; diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/BuiltInDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/BuiltInDetail.java new file mode 100644 index 00000000..6f706069 --- /dev/null +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/BuiltInDetail.java @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.metadata; + +public record BuiltInDetail() implements RelationDetail { + public static final BuiltInDetail INSTANCE = new BuiltInDetail(); +} diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ClassDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ClassDetail.java new file mode 100644 index 00000000..1d3190f5 --- /dev/null +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ClassDetail.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.metadata; + +public record ClassDetail(boolean abstractClass) implements RelationDetail { + public static final ClassDetail CONCRETE_CLASS = new ClassDetail(false); + + public static final ClassDetail ABSTRACT_CLASS = new ClassDetail(true); + + public static ClassDetail ofAbstractClass(boolean abstractClass) { + return abstractClass ? ABSTRACT_CLASS : CONCRETE_CLASS; + } +} diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java index 811ac2c0..d2dcb43a 100644 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java @@ -6,7 +6,7 @@ package tools.refinery.language.semantics.metadata; public sealed interface Metadata permits NodeMetadata, RelationMetadata { - String fullyQualifiedName(); + String name(); String simpleName(); } 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 new file mode 100644 index 00000000..0c18b1b3 --- /dev/null +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java @@ -0,0 +1,181 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.metadata; + +import com.google.inject.Inject; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.naming.IQualifiedNameProvider; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.scoping.IScope; +import org.eclipse.xtext.scoping.IScopeProvider; +import tools.refinery.language.model.problem.*; +import tools.refinery.language.semantics.model.ModelInitializer; +import tools.refinery.language.semantics.model.TracedException; +import tools.refinery.language.utils.ProblemUtil; +import tools.refinery.store.reasoning.representation.PartialRelation; + +import java.util.*; + +public class MetadataCreator { + @Inject + private IScopeProvider scopeProvider; + + @Inject + private IQualifiedNameProvider qualifiedNameProvider; + + @Inject + private IQualifiedNameConverter qualifiedNameConverter; + + private ModelInitializer initializer; + + private IScope nodeScope; + + private IScope relationScope; + + public void setInitializer(ModelInitializer initializer) { + if (initializer == null) { + throw new IllegalArgumentException("Initializer was already set"); + } + this.initializer = initializer; + var problem = initializer.getProblem(); + nodeScope = scopeProvider.getScope(problem, ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE); + relationScope = scopeProvider.getScope(problem, ProblemPackage.Literals.ASSERTION__RELATION); + } + + public List<NodeMetadata> getNodesMetadata() { + var nodes = new NodeMetadata[initializer.getNodeCount()]; + for (var entry : initializer.getNodeTrace().keyValuesView()) { + var node = entry.getOne(); + var id = entry.getTwo(); + nodes[id] = getNodeMetadata(node); + } + return List.of(nodes); + } + + private NodeMetadata getNodeMetadata(Node node) { + var qualifiedName = getQualifiedName(node); + var simpleName = getSimpleName(node, qualifiedName, nodeScope); + return new NodeMetadata(qualifiedNameConverter.toString(qualifiedName), + qualifiedNameConverter.toString(simpleName), getNodeKind(node)); + } + + private NodeKind getNodeKind(Node node) { + if (ProblemUtil.isImplicitNode(node)) { + return NodeKind.IMPLICIT; + } else if (ProblemUtil.isIndividualNode(node)) { + return NodeKind.INDIVIDUAL; + } else if (ProblemUtil.isNewNode(node)) { + return NodeKind.NEW; + } else { + throw new TracedException(node, "Unknown node type"); + } + } + + public List<RelationMetadata> getRelationsMetadata() { + var relationTrace = initializer.getRelationTrace(); + var relations = new ArrayList<RelationMetadata>(relationTrace.size()); + for (var entry : relationTrace.entrySet()) { + var relation = entry.getKey(); + var partialRelation = entry.getValue(); + var metadata = getRelationMetadata(relation, partialRelation); + relations.add(metadata); + } + return Collections.unmodifiableList(relations); + } + + private RelationMetadata getRelationMetadata(Relation relation, PartialRelation partialRelation) { + var qualifiedName = getQualifiedName(relation); + var qualifiedNameString = qualifiedNameConverter.toString(qualifiedName); + var simpleName = getSimpleName(relation, qualifiedName, relationScope); + var simpleNameString = qualifiedNameConverter.toString(simpleName); + var arity = partialRelation.arity(); + var detail = getRelationDetail(relation, partialRelation); + return new RelationMetadata(qualifiedNameString, simpleNameString, arity, detail); + } + + private RelationDetail getRelationDetail(Relation relation, PartialRelation partialRelation) { + if (ProblemUtil.isBuiltIn(relation) && !ProblemUtil.isError(relation)) { + return getBuiltInDetail(); + } + if (relation instanceof ClassDeclaration classDeclaration) { + return getClassDetail(classDeclaration); + } else if (relation instanceof ReferenceDeclaration) { + return getReferenceDetail(partialRelation); + } else if (relation instanceof EnumDeclaration) { + return getEnumDetail(); + } else if (relation instanceof PredicateDefinition predicateDefinition) { + return getPredicateDetail(predicateDefinition); + } else { + throw new TracedException(relation, "Unknown relation"); + } + } + + private RelationDetail getBuiltInDetail() { + return BuiltInDetail.INSTANCE; + } + + private RelationDetail getClassDetail(ClassDeclaration classDeclaration) { + return ClassDetail.ofAbstractClass(classDeclaration.isAbstract()); + } + + private RelationDetail getReferenceDetail(PartialRelation partialRelation) { + var metamodel = initializer.getMetamodel(); + var opposite = metamodel.oppositeReferences().get(partialRelation); + if (opposite == null) { + boolean isContainment = metamodel.containmentHierarchy().containsKey(partialRelation); + return ReferenceDetail.ofContainment(isContainment); + } else { + boolean isContainer = metamodel.containmentHierarchy().containsKey(opposite); + return new OppositeReferenceDetail(isContainer, opposite.name()); + } + } + + private RelationDetail getEnumDetail() { + return ClassDetail.CONCRETE_CLASS; + } + + private RelationDetail getPredicateDetail(PredicateDefinition predicate) { + return PredicateDetail.ofError(predicate.isError()); + } + + private QualifiedName getQualifiedName(EObject eObject) { + var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(eObject); + if (qualifiedName == null) { + throw new TracedException(eObject, "Unknown qualified name"); + } + return qualifiedName; + } + + private QualifiedName getSimpleName(EObject eObject, QualifiedName qualifiedName, IScope scope) { + var descriptions = scope.getElements(eObject); + var names = new HashSet<QualifiedName>(); + for (var description : descriptions) { + // {@code getQualifiedName()} will refer to the full name for objects that are loaded from the global + // scope, but {@code getName()} returns the qualified name that we set in + // {@code ProblemResourceDescriptionStrategy}. + names.add(description.getName()); + } + var iterator = names.stream().sorted(Comparator.comparingInt(QualifiedName::getSegmentCount)).iterator(); + while (iterator.hasNext()) { + var simpleName = iterator.next(); + if (names.contains(simpleName) && isUnique(scope, simpleName)) { + return simpleName; + } + } + throw new TracedException(eObject, "Ambiguous qualified name: " + + qualifiedNameConverter.toString(qualifiedName)); + } + + private boolean isUnique(IScope scope, QualifiedName name) { + var iterator = scope.getElements(name).iterator(); + if (!iterator.hasNext()) { + return false; + } + iterator.next(); + return !iterator.hasNext(); + } +} diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java index 27a86cb3..01f0cd09 100644 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java @@ -8,5 +8,5 @@ package tools.refinery.language.semantics.metadata; public enum NodeKind { IMPLICIT, INDIVIDUAL, - ENUM_LITERAL + NEW } diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java index 8d91273c..812952c0 100644 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java @@ -5,5 +5,5 @@ */ package tools.refinery.language.semantics.metadata; -public record NodeMetadata(String fullyQualifiedName, String simpleName, NodeKind kind) implements Metadata { +public record NodeMetadata(String name, String simpleName, NodeKind kind) implements Metadata { } diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/OppositeReferenceDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/OppositeReferenceDetail.java new file mode 100644 index 00000000..26d7461c --- /dev/null +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/OppositeReferenceDetail.java @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.metadata; + +public record OppositeReferenceDetail(boolean container, String opposite) implements RelationDetail { +} diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/PredicateDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/PredicateDetail.java new file mode 100644 index 00000000..ca397eca --- /dev/null +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/PredicateDetail.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.metadata; + +public record PredicateDetail(boolean error) implements RelationDetail { + public static final PredicateDetail PREDICATE = new PredicateDetail(false); + + public static final PredicateDetail ERROR_PREDICATE = new PredicateDetail(true); + + public static PredicateDetail ofError(boolean error) { + return error ? ERROR_PREDICATE : PREDICATE; + } +} diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ReferenceDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ReferenceDetail.java new file mode 100644 index 00000000..36771566 --- /dev/null +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ReferenceDetail.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.metadata; + +public record ReferenceDetail(boolean containment) implements RelationDetail { + public static final ReferenceDetail CROSS_REFERENCE = new ReferenceDetail(false); + + public static final ReferenceDetail CONTAINMENT_REFERENCE = new ReferenceDetail(true); + + public static ReferenceDetail ofContainment(boolean containment) { + return containment ? CONTAINMENT_REFERENCE : CROSS_REFERENCE; + } +} diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationDetail.java new file mode 100644 index 00000000..105179fd --- /dev/null +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationDetail.java @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.metadata; + +public sealed interface RelationDetail permits ClassDetail, ReferenceDetail, PredicateDetail, OppositeReferenceDetail, + BuiltInDetail { +} diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationKind.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationKind.java deleted file mode 100644 index 28a3c565..00000000 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationKind.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.semantics.metadata; - -public enum RelationKind { - BUILTIN, - CLASS, - ENUM, - REFERENCE, - OPPOSITE, - CONTAINMENT, - CONTAINER, - PREDICATE, - ERROR -} diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java index 62de6031..5abcc253 100644 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java @@ -5,6 +5,5 @@ */ package tools.refinery.language.semantics.metadata; -public record RelationMetadata(String fullyQualifiedName, String simpleName, int arity, RelationKind kind, - String opposite) implements Metadata { +public record RelationMetadata(String name, String simpleName, int arity, RelationDetail detail) implements Metadata { } 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 82746aee..aaef3326 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 @@ -64,7 +64,7 @@ public class ModelInitializer { private final Map<PartialRelation, RelationInfo> partialRelationInfoMap = new HashMap<>(); - private Map<AnyPartialSymbol, Relation> inverseTrace = new HashMap<>(); + private final Map<AnyPartialSymbol, Relation> inverseTrace = new HashMap<>(); private Map<Relation, PartialRelation> relationTrace; @@ -74,6 +74,10 @@ public class ModelInitializer { private ModelSeed modelSeed; + public Problem getProblem() { + return problem; + } + public int getNodeCount() { return nodeTrace.size(); } @@ -90,6 +94,10 @@ public class ModelInitializer { return inverseTrace.get(partialRelation); } + public Metamodel getMetamodel() { + return metamodel; + } + public ModelSeed createModel(Problem problem, ModelStoreBuilder storeBuilder) { this.problem = problem; this.storeBuilder = storeBuilder; diff --git a/subprojects/language-web/build.gradle.kts b/subprojects/language-web/build.gradle.kts index 547cb089..a4ccdd9f 100644 --- a/subprojects/language-web/build.gradle.kts +++ b/subprojects/language-web/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(project(":refinery-language-ide")) implementation(project(":refinery-language-semantics")) implementation(project(":refinery-store-query-viatra")) + implementation(libs.gson) 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/ProblemWebSocketServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java index 7b48cde8..e98d115e 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java @@ -10,8 +10,10 @@ import org.eclipse.xtext.util.DisposableRegistry; import jakarta.servlet.ServletException; import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; -public class ProblemWebSocketServlet extends XtextWebSocketServlet { +import java.io.Serial; +public class ProblemWebSocketServlet extends XtextWebSocketServlet { + @Serial private static final long serialVersionUID = -7040955470384797008L; private transient DisposableRegistry disposableRegistry; 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 56b2cbc1..ba55dc77 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 @@ -55,7 +55,7 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { } var problem = getProblem(doc); if (problem == null) { - return new SemanticsSuccessResult(List.of(), new JsonObject()); + return new SemanticsSuccessResult(List.of(), List.of(), new JsonObject()); } var worker = workerProvider.get(); worker.setProblem(problem, cancelIndicator); 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 index 15fd4b55..350b0b2b 100644 --- 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 @@ -6,8 +6,11 @@ package tools.refinery.language.web.semantics; import com.google.gson.JsonObject; +import tools.refinery.language.semantics.metadata.NodeMetadata; +import tools.refinery.language.semantics.metadata.RelationMetadata; import java.util.List; -public record SemanticsSuccessResult(List<String> nodes, JsonObject partialInterpretation) implements SemanticsResult { +public record SemanticsSuccessResult(List<NodeMetadata> nodes, List<RelationMetadata> relations, + JsonObject partialInterpretation) implements SemanticsResult { } 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 43d0238c..108b87dc 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 @@ -18,6 +18,7 @@ import org.eclipse.xtext.validation.IDiagnosticConverter; import org.eclipse.xtext.validation.Issue; import org.eclipse.xtext.web.server.validation.ValidationResult; import tools.refinery.language.model.problem.Problem; +import tools.refinery.language.semantics.metadata.MetadataCreator; import tools.refinery.language.semantics.model.ModelInitializer; import tools.refinery.language.semantics.model.SemanticsUtils; import tools.refinery.language.semantics.model.TracedException; @@ -34,8 +35,6 @@ import tools.refinery.store.tuple.Tuple; import tools.refinery.viatra.runtime.CancellationToken; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.TreeMap; import java.util.concurrent.Callable; @@ -54,6 +53,9 @@ class SemanticsWorker implements Callable<SemanticsResult> { @Inject private ModelInitializer initializer; + @Inject + private MetadataCreator metadataCreator; + private Problem problem; private CancellationToken cancellationToken; @@ -78,7 +80,11 @@ class SemanticsWorker implements Callable<SemanticsResult> { try { var modelSeed = initializer.createModel(problem, builder); cancellationToken.checkCancelled(); - var nodeTrace = getNodeTrace(initializer); + metadataCreator.setInitializer(initializer); + cancellationToken.checkCancelled(); + var nodesMetadata = metadataCreator.getNodesMetadata(); + cancellationToken.checkCancelled(); + var relationsMetadata = metadataCreator.getRelationsMetadata(); cancellationToken.checkCancelled(); var store = builder.build(); cancellationToken.checkCancelled(); @@ -87,7 +93,7 @@ class SemanticsWorker implements Callable<SemanticsResult> { cancellationToken.checkCancelled(); var partialInterpretation = getPartialInterpretation(initializer, model); - return new SemanticsSuccessResult(nodeTrace, partialInterpretation); + return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation); } catch (TracedException e) { return getTracedErrorResult(e.getSourceElement(), e.getMessage()); } catch (TranslationException e) { @@ -96,16 +102,6 @@ class SemanticsWorker implements Callable<SemanticsResult> { } } - private List<String> 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) { var adapter = model.getAdapter(ReasoningAdapter.class); var json = new JsonObject(); diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java index ff788e94..7c4562bf 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java @@ -5,19 +5,22 @@ */ package tools.refinery.language.web.xtext.server.message; +import com.google.gson.annotations.SerializedName; + import java.util.Map; import java.util.Objects; -import com.google.gson.annotations.SerializedName; - public class XtextWebRequest { private String id; @SerializedName("request") private Map<String, String> requestData; + public XtextWebRequest() { + this(null, null); + } + public XtextWebRequest(String id, Map<String, String> requestData) { - super(); this.id = id; this.requestData = requestData; } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java new file mode 100644 index 00000000..b16cf7df --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2011 Google Inc. + * Copyright (C) 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file was copied into Refinery according to upstream instructions at + * https://github.com/google/gson/issues/1104#issuecomment-309582470. + * However, we changed the package name below to avoid potential clashes + * with other jars on the classpath. + */ +package tools.refinery.language.web.xtext.servlet; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + * <pre> {@code + * abstract class Shape { + * int x; + * int y; + * } + * class Circle extends Shape { + * int radius; + * } + * class Rectangle extends Shape { + * int width; + * int height; + * } + * class Diamond extends Shape { + * int width; + * int height; + * } + * class Drawing { + * Shape bottomShape; + * Shape topShape; + * } + * }</pre> + * <p>Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond? <pre> {@code + * { + * "bottomShape": { + * "width": 10, + * "height": 5, + * "x": 0, + * "y": 0 + * }, + * "topShape": { + * "radius": 2, + * "x": 4, + * "y": 1 + * } + * }}</pre> + * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized: <pre> {@code + * { + * "bottomShape": { + * "type": "Diamond", + * "width": 10, + * "height": 5, + * "x": 0, + * "y": 0 + * }, + * "topShape": { + * "type": "Circle", + * "radius": 2, + * "x": 4, + * "y": 1 + * } + * }}</pre> + * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + * <h2>Registering Types</h2> + * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used. <pre> {@code + * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory + * = RuntimeTypeAdapterFactory.of(Shape.class, "type"); + * }</pre> + * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + * <pre> {@code + * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle"); + * shapeAdapterFactory.registerSubtype(Circle.class, "Circle"); + * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond"); + * }</pre> + * Finally, register the type adapter factory in your application's GSON builder: + * <pre> {@code + * Gson gson = new GsonBuilder() + * .registerTypeAdapterFactory(shapeAdapterFactory) + * .create(); + * }</pre> + * Like {@code GsonBuilder}, this API supports chaining: <pre> {@code + * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class) + * .registerSubtype(Rectangle.class) + * .registerSubtype(Circle.class) + * .registerSubtype(Diamond.class); + * }</pre> + * + * <h2>Serialization and deserialization</h2> + * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + * <pre> {@code + * Diamond diamond = new Diamond(); + * String json = gson.toJson(diamond, Shape.class); + * }</pre> + * And then: + * <pre> {@code + * Shape shape = gson.fromJson(json, Shape.class); + * }</pre> + */ +public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory { + private final Class<?> baseType; + private final String typeFieldName; + private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>(); + private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>(); + private final boolean maintainType; + private boolean recognizeSubtypes; + + private RuntimeTypeAdapterFactory( + Class<?> baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + * + * @param maintainType true if the type field should be included in deserialized objects + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) { + return new RuntimeTypeAdapterFactory<>(baseType, "type", false); + } + + /** + * Ensures that this factory will handle not just the given {@code baseType}, but any subtype + * of that type. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory<T> recognizeSubtypes() { + this.recognizeSubtypes = true; + return this; + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) { + return registerSubtype(type, type.getSimpleName()); + } + + @Override + public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) { + if (type == null) { + return null; + } + Class<?> rawType = type.getRawType(); + boolean handle = + recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType); + if (!handle) { + return null; + } + + final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class); + final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>(); + final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>(); + for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) { + TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter<R>() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = jsonElementAdapter.read(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class<?> srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + jsonElementAdapter.write(out, jsonObject); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + jsonElementAdapter.write(out, clone); + } + }.nullSafe(); + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java index 923fecd6..1fde1be5 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java @@ -6,6 +6,7 @@ package tools.refinery.language.web.xtext.servlet; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonIOException; import com.google.gson.JsonParseException; import org.eclipse.jetty.websocket.api.Callback; @@ -16,6 +17,7 @@ import org.eclipse.xtext.resource.IResourceServiceProvider; import org.eclipse.xtext.web.server.ISession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.refinery.language.semantics.metadata.*; import tools.refinery.language.web.xtext.server.ResponseHandler; import tools.refinery.language.web.xtext.server.ResponseHandlerException; import tools.refinery.language.web.xtext.server.TransactionExecutor; @@ -28,7 +30,15 @@ import java.io.Reader; public class XtextWebSocket implements ResponseHandler { private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class); - private final Gson gson = new Gson(); + private final Gson gson = new GsonBuilder() + .disableJdkUnsafe() + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(RelationDetail.class, "type") + .registerSubtype(ClassDetail.class, "class") + .registerSubtype(ReferenceDetail.class, "reference") + .registerSubtype(OppositeReferenceDetail.class, "opposite") + .registerSubtype(PredicateDetail.class, "predicate") + .registerSubtype(BuiltInDetail.class, "builtin")) + .create(); private final TransactionExecutor executor; 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 03b0c729..a9efc4bb 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java @@ -7,7 +7,6 @@ package tools.refinery.language.utils; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; - import tools.refinery.language.model.problem.*; public final class ProblemUtil { @@ -50,6 +49,10 @@ public final class ProblemUtil { } } + public static boolean isError(EObject eObject) { + return eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError(); + } + public static boolean isIndividualNode(Node node) { var containingFeature = node.eContainingFeature(); return containingFeature == ProblemPackage.Literals.INDIVIDUAL_DECLARATION__NODES diff --git a/yarn.lock b/yarn.lock index 59835487..bc3b3de2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4004,7 +4004,7 @@ __metadata: "d3-graphviz@patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch": version: 5.1.0 - resolution: "d3-graphviz@patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch::version=5.1.0&hash=d00cb5" + resolution: "d3-graphviz@patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch::version=5.1.0&hash=dcacac" dependencies: "@hpcc-js/wasm": "npm:2.13.1" d3-dispatch: "npm:^3.0.1" @@ -4016,7 +4016,7 @@ __metadata: d3-zoom: "npm:^3.0.0" peerDependencies: d3-selection: ^3.0.0 - checksum: 23e56b979950ff19f12321e9c23e56e55e791950f42ced3613581f4ac6a70e7b78b4bf3c600377df0766ee20f967741c939011b7a4d192a9eb3e2e07fa45833d + checksum: 47ac96385ebee243fa44898f0f4cd25dce49683d66955511adaf94a584ae7261a485cbcec8910709dd5a6fe857ae7b7e05abe5b1ce0f0e9b69d2691ff0b13d81 languageName: node linkType: hard -- cgit v1.2.3-54-g00ecf From 5e21737b0a8a903d2e37e821a96a739eb7429fcd Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Tue, 29 Aug 2023 12:05:08 +0200 Subject: refactor: rename invalidNumberOfContainers Use a simplified name invalidContainer, since this pattern may match even if there is a sufficient number of ERROR containers. --- .../tools/refinery/language/semantics/model/ModelInitializer.java | 4 ++-- .../main/java/tools/refinery/language/utils/BuiltinSymbols.java | 2 +- .../main/java/tools/refinery/language/utils/ProblemDesugarer.java | 5 ++--- .../src/main/resources/tools/refinery/language/builtin.problem | 2 +- .../translator/containment/ContainmentHierarchyTranslator.java | 8 ++++---- 5 files changed, 10 insertions(+), 11 deletions(-) (limited to 'subprojects/language/src/main') 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 aaef3326..13e25d0a 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 @@ -115,8 +115,8 @@ public class ModelInitializer { null, TruthValue.UNKNOWN)); putRelationInfo(builtinSymbols.contains(), new RelationInfo(ContainmentHierarchyTranslator.CONTAINS_SYMBOL, null, TruthValue.UNKNOWN)); - putRelationInfo(builtinSymbols.invalidNumberOfContainers(), - new RelationInfo(ContainmentHierarchyTranslator.INVALID_NUMBER_OF_CONTAINERS, TruthValue.FALSE, + putRelationInfo(builtinSymbols.invalidContainer(), + new RelationInfo(ContainmentHierarchyTranslator.INVALID_CONTAINER, TruthValue.FALSE, TruthValue.FALSE)); collectNodes(); collectPartialSymbols(); diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/BuiltinSymbols.java b/subprojects/language/src/main/java/tools/refinery/language/utils/BuiltinSymbols.java index 70a86b51..c87fa044 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/BuiltinSymbols.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/BuiltinSymbols.java @@ -9,5 +9,5 @@ import tools.refinery.language.model.problem.*; public record BuiltinSymbols(Problem problem, ClassDeclaration node, PredicateDefinition equals, PredicateDefinition exists, ClassDeclaration contained, PredicateDefinition contains, - PredicateDefinition invalidNumberOfContainers) { + PredicateDefinition invalidContainer) { } diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java index 9f08654c..59e26561 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java @@ -43,9 +43,8 @@ public class ProblemDesugarer { var exists = doGetDeclaration(builtin, PredicateDefinition.class, "exists"); var contained = doGetDeclaration(builtin, ClassDeclaration.class, "contained"); var contains = doGetDeclaration(builtin, PredicateDefinition.class, "contains"); - var invalidNumberOfContainers = doGetDeclaration(builtin, PredicateDefinition.class, - "invalidNumberOfContainers"); - return new BuiltinSymbols(builtin, node, equals, exists, contained, contains, invalidNumberOfContainers); + var invalidContainer = doGetDeclaration(builtin, PredicateDefinition.class, "invalidContainer"); + return new BuiltinSymbols(builtin, node, equals, exists, contained, contains, invalidContainer); } private <T extends Statement & NamedElement> T doGetDeclaration(Problem builtin, Class<T> type, String name) { diff --git a/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem b/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem index 8570e363..022c3167 100644 --- a/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem +++ b/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem @@ -13,4 +13,4 @@ abstract class contained extends node. pred contains(container, contained contained). -error invalidNumberOfContainers(contained contained). +error invalidContainer(contained contained). diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java index 37eac022..dda9f2c8 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java @@ -42,7 +42,7 @@ import static tools.refinery.store.reasoning.literal.PartialLiterals.must; public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { public static final PartialRelation CONTAINED_SYMBOL = new PartialRelation("contained", 1); - public static final PartialRelation INVALID_NUMBER_OF_CONTAINERS = new PartialRelation("invalidNumberOfContainers", + public static final PartialRelation INVALID_CONTAINER = new PartialRelation("invalidContainer", 1); public static final PartialRelation CONTAINS_SYMBOL = new PartialRelation("contains", 2); @@ -97,7 +97,7 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { public void apply(ModelStoreBuilder storeBuilder) { storeBuilder.symbol(containsStorage); translateContains(storeBuilder); - translateInvalidNumberOfContainers(storeBuilder); + translateInvalidContainer(storeBuilder); for (var entry : containmentInfoMap.entrySet()) { var linkType = entry.getKey(); var info = entry.getValue(); @@ -212,8 +212,8 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { )))); } - private void translateInvalidNumberOfContainers(ModelStoreBuilder storeBuilder) { + private void translateInvalidContainer(ModelStoreBuilder storeBuilder) { storeBuilder.with(new InvalidMultiplicityErrorTranslator(CONTAINED_SYMBOL, CONTAINS_SYMBOL, true, - ConstrainedMultiplicity.of(CardinalityIntervals.ONE, INVALID_NUMBER_OF_CONTAINERS))); + ConstrainedMultiplicity.of(CardinalityIntervals.ONE, INVALID_CONTAINER))); } } -- cgit v1.2.3-54-g00ecf