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 --- .../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 +- 10 files changed, 224 insertions(+), 30 deletions(-) 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-web/src/main/java/tools') 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); } } -- cgit v1.2.3-54-g00ecf From 263e7c025ab19573fba087f050b9dc1f1ade193f Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 17 Aug 2023 17:07:12 +0200 Subject: fix: containment link translation Improper equality implementation of representative election literals merged weak and strong connected components during query canonicalization. --- .../language/web/semantics/SemanticsService.java | 12 ++++-- .../literal/RepresentativeElectionLiteral.java | 16 ++++++++ .../ContainmentHierarchyTranslator.java | 4 +- .../translator/metamodel/MetamodelTest.java | 45 ++++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) (limited to 'subprojects/language-web/src/main/java/tools') 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 483d24f6..eb7ab204 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java @@ -54,7 +54,10 @@ public class SemanticsService extends AbstractCachedService { @Override public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) { - long start = System.currentTimeMillis(); + long start = 0; + if (LOG.isTraceEnabled()) { + start = System.currentTimeMillis(); + } Problem problem = getProblem(doc, cancelIndicator); if (problem == null) { return null; @@ -73,8 +76,11 @@ public class SemanticsService extends AbstractCachedService { 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); + if (LOG.isTraceEnabled()) { + long end = System.currentTimeMillis(); + LOG.trace("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); diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java index 5d57c06c..f6545f9f 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java @@ -6,6 +6,8 @@ package tools.refinery.store.query.literal; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.equality.LiteralEqualityHelper; +import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; import tools.refinery.store.query.term.NodeVariable; import tools.refinery.store.query.term.ParameterDirection; @@ -81,6 +83,20 @@ public class RepresentativeElectionLiteral extends AbstractCallLiteral { return new RepresentativeElectionLiteral(connectivity, newTarget, newArguments); } + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherRepresentativeElectionLiteral = (RepresentativeElectionLiteral) other; + return connectivity.equals(otherRepresentativeElectionLiteral.connectivity); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return super.hashCodeWithSubstitution(helper) * 31 + connectivity.hashCode(); + } + @Override public String toString() { var builder = new StringBuilder(); 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 9d1b8cf4..eb112d0e 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 @@ -156,7 +156,7 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { ) .clause(representative -> List.of( mayNewHelper.call(parent, child), - weakComponents.call(child, representative), + weakComponents.call(parent, representative), // Violation of para-consistency: // If there is a surely existing node with at least two containers, its (transitive) containers // will end up in the same weakly connected component, and we will spuriously mark the @@ -178,7 +178,7 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { .clause(representative -> List.of( mayExistingHelper.call(parent, child), strongComponents.call(parent, representative), - not(strongComponents.call(parent, representative)) + not(strongComponents.call(child, representative)) ))); storeBuilder.with(PartialRelationTranslator.of(linkType) diff --git a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelTest.java b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelTest.java index 9e74cf02..87e28e80 100644 --- a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelTest.java +++ b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelTest.java @@ -105,4 +105,49 @@ class MetamodelTest { assertThat(enrolledStudentsInterpretation.get(Tuple.of(1, 3)), is(TruthValue.FALSE)); assertThat(enrolledStudentsInterpretation.get(Tuple.of(1, 4)), is(TruthValue.UNKNOWN)); } + + @Test + void simpleContainmentTest() { + var metamodel = Metamodel.builder() + .type(university) + .type(course) + .reference(courses, university, true, course) + .build(); + + var store = ModelStore.builder() + .with(ViatraModelQueryAdapter.builder()) + .with(ReasoningAdapter.builder()) + .with(new MultiObjectTranslator()) + .with(new MetamodelTranslator(metamodel)) + .build(); + + var seed = ModelSeed.builder(4) + .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder + .reducedValue(CardinalityIntervals.ONE) + .put(Tuple.of(0), CardinalityIntervals.SET) + .put(Tuple.of(1), CardinalityIntervals.SET)) + .seed(ContainmentHierarchyTranslator.CONTAINED_SYMBOL, builder -> builder + .reducedValue(TruthValue.UNKNOWN)) + .seed(ContainmentHierarchyTranslator.CONTAINS_SYMBOL, builder -> builder + .reducedValue(TruthValue.UNKNOWN)) + .seed(university, builder -> builder + .reducedValue(TruthValue.UNKNOWN) + .put(Tuple.of(0), TruthValue.TRUE)) + .seed(course, builder -> builder + .reducedValue(TruthValue.UNKNOWN) + .put(Tuple.of(1), TruthValue.TRUE)) + .seed(courses, builder -> builder + .reducedValue(TruthValue.UNKNOWN) + .put(Tuple.of(2, 3), TruthValue.TRUE)) + .build(); + + var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(seed); + var coursesInterpretation = model.getAdapter(ReasoningAdapter.class) + .getPartialInterpretation(Concreteness.PARTIAL, courses); + + assertThat(coursesInterpretation.get(Tuple.of(0, 1)), is(TruthValue.UNKNOWN)); + assertThat(coursesInterpretation.get(Tuple.of(0, 3)), is(TruthValue.FALSE)); + assertThat(coursesInterpretation.get(Tuple.of(2, 1)), is(TruthValue.UNKNOWN)); + assertThat(coursesInterpretation.get(Tuple.of(2, 3)), is(TruthValue.TRUE)); + } } -- cgit v1.2.3-54-g00ecf From 37340f19b0f561c0d85f58834e8f716ba6234f10 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 17 Aug 2023 18:16:48 +0200 Subject: fix: opposite translation error reporting --- subprojects/frontend/src/index.tsx | 9 +--- .../language/web/semantics/SemanticsService.java | 2 +- .../translator/metamodel/MetamodelBuilder.java | 6 +-- .../translator/metamodel/MetamodelBuilderTest.java | 57 ++++++++++++++++++++++ .../translator/metamodel/MetamodelTest.java | 29 ++++++----- 5 files changed, 77 insertions(+), 26 deletions(-) create mode 100644 subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilderTest.java (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx index a2746748..077bae79 100644 --- a/subprojects/frontend/src/index.tsx +++ b/subprojects/frontend/src/index.tsx @@ -12,11 +12,12 @@ import RootStore from './RootStore'; const initialValue = `// Metamodel class Person { + contains Post[] posts opposite author Person[] friend opposite friend } class Post { - Person[1] author + container Person[1] author opposite posts Post replyTo } @@ -30,15 +31,9 @@ error replyToNotFriend(Post x, Post y) <-> error replyToCycle(Post x) <-> replyTo+(x, x). // Instance model -Person(a). -Person(b). friend(a, b). -friend(b, a). -Post(p1). author(p1, a). -Post(p2). author(p2, b). -replyTo(p2, p1). !author(Post::new, a). 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 eb7ab204..79ca32ff 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 @@ -84,7 +84,7 @@ public class SemanticsService extends AbstractCachedService { return new SemanticsSuccessResult(nodeTrace, partialInterpretation); } catch (RuntimeException e) { LOG.error("Error while computing semantics", e); - return new SemanticsErrorResult(e.toString()); + return new SemanticsErrorResult(e.getMessage()); } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java index 92370e25..d0732edc 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java @@ -203,15 +203,15 @@ public class MetamodelBuilder { throw new IllegalArgumentException("Opposite %s of %s is not defined" .formatted(opposite, linkType)); } - if (!oppositeInfo.opposite().equals(linkType)) { + if (!linkType.equals(oppositeInfo.opposite())) { throw new IllegalArgumentException("Expected %s to have opposite %s, got %s instead" .formatted(opposite, linkType, oppositeInfo.opposite())); } - if (!oppositeInfo.sourceType().equals(targetType)) { + if (!targetType.equals(oppositeInfo.sourceType())) { throw new IllegalArgumentException("Expected %s to have source type %s, got %s instead" .formatted(opposite, targetType, oppositeInfo.sourceType())); } - if (!oppositeInfo.targetType().equals(sourceType)) { + if (!sourceType.equals(oppositeInfo.targetType())) { throw new IllegalArgumentException("Expected %s to have target type %s, got %s instead" .formatted(opposite, sourceType, oppositeInfo.targetType())); } diff --git a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilderTest.java b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilderTest.java new file mode 100644 index 00000000..115ba8cd --- /dev/null +++ b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilderTest.java @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.reasoning.translator.metamodel; + +import org.junit.jupiter.api.Test; +import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.multiplicity.ConstrainedMultiplicity; +import tools.refinery.store.representation.cardinality.CardinalityIntervals; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MetamodelBuilderTest { + private final PartialRelation university = new PartialRelation("University", 1); + private final PartialRelation course = new PartialRelation("Course", 1); + private final PartialRelation courses = new PartialRelation("courses", 2); + private final PartialRelation location = new PartialRelation("location", 2); + + @Test + void missingOppositeTest() { + var builder = Metamodel.builder() + .type(university) + .type(course) + .reference(courses, university, course, location) + .reference(location, course, university); + + assertThrows(IllegalArgumentException.class, builder::build); + } + + @Test + void invalidOppositeTypeTest() { + var builder = Metamodel.builder() + .type(university) + .type(course) + .reference(courses, university, course, location) + .reference(location, course, course, courses); + + assertThrows(IllegalArgumentException.class, builder::build); + } + + @Test + void invalidOppositeMultiplicityTest() { + var invalidMultiplicity = new PartialRelation("invalidMultiplicity", 1); + + var builder = Metamodel.builder() + .type(university) + .type(course) + .reference(courses, university, true, course, location) + .reference(location, course, + ConstrainedMultiplicity.of(CardinalityIntervals.atLeast(2), invalidMultiplicity), + university, courses); + + assertThrows(IllegalArgumentException.class, builder::build); + } +} diff --git a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelTest.java b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelTest.java index 87e28e80..eabbdffe 100644 --- a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelTest.java +++ b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelTest.java @@ -6,6 +6,7 @@ package tools.refinery.store.reasoning.translator.metamodel; import org.junit.jupiter.api.Test; +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; @@ -52,13 +53,6 @@ class MetamodelTest { ConstrainedMultiplicity.of(CardinalityIntervals.SOME, invalidStudentCount), student) .build(); - var store = ModelStore.builder() - .with(ViatraModelQueryAdapter.builder()) - .with(ReasoningAdapter.builder()) - .with(new MultiObjectTranslator()) - .with(new MetamodelTranslator(metamodel)) - .build(); - var seed = ModelSeed.builder(5) .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder .reducedValue(CardinalityIntervals.ONE) @@ -87,7 +81,7 @@ class MetamodelTest { .seed(enrolledStudents, builder -> builder.reducedValue(TruthValue.UNKNOWN)) .build(); - var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(seed); + var model = createModel(metamodel, seed); var reasoningAdapter = model.getAdapter(ReasoningAdapter.class); var coursesInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.PARTIAL, courses); @@ -114,12 +108,6 @@ class MetamodelTest { .reference(courses, university, true, course) .build(); - var store = ModelStore.builder() - .with(ViatraModelQueryAdapter.builder()) - .with(ReasoningAdapter.builder()) - .with(new MultiObjectTranslator()) - .with(new MetamodelTranslator(metamodel)) - .build(); var seed = ModelSeed.builder(4) .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder @@ -141,7 +129,7 @@ class MetamodelTest { .put(Tuple.of(2, 3), TruthValue.TRUE)) .build(); - var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(seed); + var model = createModel(metamodel, seed); var coursesInterpretation = model.getAdapter(ReasoningAdapter.class) .getPartialInterpretation(Concreteness.PARTIAL, courses); @@ -150,4 +138,15 @@ class MetamodelTest { assertThat(coursesInterpretation.get(Tuple.of(2, 1)), is(TruthValue.UNKNOWN)); assertThat(coursesInterpretation.get(Tuple.of(2, 3)), is(TruthValue.TRUE)); } + + private static Model createModel(Metamodel metamodel, ModelSeed seed) { + var store = ModelStore.builder() + .with(ViatraModelQueryAdapter.builder()) + .with(ReasoningAdapter.builder()) + .with(new MultiObjectTranslator()) + .with(new MetamodelTranslator(metamodel)) + .build(); + + return store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(seed); + } } -- cgit v1.2.3-54-g00ecf From 8b4b16665868aee08ac5a90398104a1c07ae0365 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 17 Aug 2023 20:21:03 +0200 Subject: fix: more precise new object types A new object should not be an instance of any subclasses. Also, it should be possible cleare a node without any other type. --- .../language/semantics/model/ModelInitializer.java | 94 +++++++++++++++------- .../language/web/semantics/SemanticsService.java | 3 +- 2 files changed, 68 insertions(+), 29 deletions(-) (limited to 'subprojects/language-web/src/main/java/tools') 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 93c7c8e5..d2990aff 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 @@ -53,10 +53,14 @@ public class ModelInitializer { private final Map relationInfoMap = new LinkedHashMap<>(); + private final Map partialRelationInfoMap = new LinkedHashMap<>(); + private Map relationTrace; private final MetamodelBuilder metamodelBuilder = Metamodel.builder(); + private Metamodel metamodel; + public int getNodeCount() { return nodeTrace.size(); } @@ -75,13 +79,15 @@ public class ModelInitializer { "Problem has no builtin library")); var nodeInfo = collectPartialRelation(builtinSymbols.node(), 1, TruthValue.TRUE, TruthValue.TRUE); nodeRelation = nodeInfo.partialRelation(); - metamodelBuilder.type(nodeRelation, true); + metamodelBuilder.type(nodeRelation); relationInfoMap.put(builtinSymbols.exists(), new RelationInfo(ReasoningAdapter.EXISTS_SYMBOL, null, TruthValue.TRUE)); - relationInfoMap.put(builtinSymbols.equals(), new RelationInfo(ReasoningAdapter.EQUALS_SYMBOL, (TruthValue) null, + 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.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(), @@ -89,9 +95,9 @@ public class ModelInitializer { TruthValue.FALSE)); collectNodes(); collectPartialSymbols(); + collectMetamodel(); + metamodel = metamodelBuilder.build(); collectAssertions(); - var metamodel = metamodelBuilder.build(); - builder.with(ReasoningAdapter.builder()); builder.with(new MultiObjectTranslator()); builder.with(new MetamodelTranslator(metamodel)); relationTrace = new LinkedHashMap<>(relationInfoMap.size()); @@ -160,11 +166,18 @@ public class ModelInitializer { } } + private void putRelationInfo(Relation relation, RelationInfo info) { + relationInfoMap.put(relation, info); + partialRelationInfoMap.put(info.partialRelation(), info); + } + 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); + var info = new RelationInfo(name, arity, value, defaultValue); + partialRelationInfoMap.put(info.partialRelation(), info); + return info; }); } @@ -172,23 +185,22 @@ public class ModelInitializer { return semanticsUtils.getName(relation).orElseGet(() -> "#" + relationInfoMap.size()); } - private void collectAssertions() { + private void collectMetamodel() { for (var statement : problem.getStatements()) { if (statement instanceof ClassDeclaration classDeclaration) { - collectClassDeclarationAssertions(classDeclaration); + collectClassDeclarationMetamodel(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); + collectEnumMetamodel(enumDeclaration); } } } - private void collectClassDeclarationAssertions(ClassDeclaration classDeclaration) { + private void collectEnumMetamodel(EnumDeclaration enumDeclaration) { + var info = getRelationInfo(enumDeclaration); + metamodelBuilder.type(info.partialRelation(), nodeRelation); + } + + private void collectClassDeclarationMetamodel(ClassDeclaration classDeclaration) { var superTypes = classDeclaration.getSuperTypes(); var partialSuperTypes = new ArrayList(superTypes.size() + 1); partialSuperTypes.add(nodeRelation); @@ -198,23 +210,17 @@ public class ModelInitializer { 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); + collectReferenceDeclarationMetamodel(classDeclaration, referenceDeclaration); } else { throw new IllegalArgumentException("Unknown feature declaration: " + featureDeclaration); } } } - private void collectReferenceDeclarationAssertions(ClassDeclaration classDeclaration, - ReferenceDeclaration referenceDeclaration) { + private void collectReferenceDeclarationMetamodel(ClassDeclaration classDeclaration, + ReferenceDeclaration referenceDeclaration) { var relation = getRelationInfo(referenceDeclaration).partialRelation(); var source = getRelationInfo(classDeclaration).partialRelation(); var target = getRelationInfo(referenceDeclaration.getReferenceType()).partialRelation(); @@ -249,15 +255,47 @@ public class ModelInitializer { return ConstrainedMultiplicity.of(interval, constraint); } + + 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); + } + } + } + + private void collectClassDeclarationAssertions(ClassDeclaration classDeclaration) { + var newNode = classDeclaration.getNewNode(); + if (newNode == null) { + return; + } + var newNodeId = getNodeId(newNode); + collectCardinalityAssertions(newNodeId, TruthValue.UNKNOWN); + var info = getRelationInfo(classDeclaration); + var tuple = Tuple.of(newNodeId); + mergeValue(classDeclaration, tuple, TruthValue.TRUE); + var typeInfo = metamodel.typeHierarchy().getAnalysisResult(info.partialRelation()); + for (var subType : typeInfo.getDirectSubtypes()) { + partialRelationInfoMap.get(subType).assertions().mergeValue(tuple, TruthValue.FALSE); + } + } + 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); } + var info = getRelationInfo(enumDeclaration); info.assertions().overwriteValues(overlay); } 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 79ca32ff..c828b3d5 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 @@ -64,7 +64,8 @@ public class SemanticsService extends AbstractCachedService { } var initializer = initializerProvider.get(); var builder = ModelStore.builder() - .with(ViatraModelQueryAdapter.builder()); + .with(ViatraModelQueryAdapter.builder()) + .with(ReasoningAdapter.builder()); operationCanceledManager.checkCanceled(cancelIndicator); try { var modelSeed = initializer.createModel(problem, builder); -- cgit v1.2.3-54-g00ecf From c64e925d15cbc187623eb5ac28e5c769d8673321 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 17 Aug 2023 21:12:45 +0200 Subject: refactor: candidate interpreation only on demand Avoid creating interpretations not needed for the graph analysis task. --- .../language/web/semantics/SemanticsService.java | 5 +++-- .../refinery/store/reasoning/ReasoningBuilder.java | 9 +++++++++ .../store/reasoning/ReasoningStoreAdapter.java | 4 ++++ .../reasoning/internal/ReasoningAdapterImpl.java | 19 ++++++++++++++----- .../reasoning/internal/ReasoningBuilderImpl.java | 21 +++++++++++++++------ .../internal/ReasoningStoreAdapterImpl.java | 13 +++++++++++-- .../interpretation/PartialInterpretation.java | 8 ++++++++ .../QueryBasedRelationInterpretationFactory.java | 15 +++++++++++++++ .../translator/PartialRelationTranslator.java | 7 ++----- .../translator/PartialSymbolTranslator.java | 3 +++ 10 files changed, 84 insertions(+), 20 deletions(-) (limited to 'subprojects/language-web/src/main/java/tools') 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 c828b3d5..2495430e 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 @@ -65,7 +65,8 @@ public class SemanticsService extends AbstractCachedService { var initializer = initializerProvider.get(); var builder = ModelStore.builder() .with(ViatraModelQueryAdapter.builder()) - .with(ReasoningAdapter.builder()); + .with(ReasoningAdapter.builder() + .requiredInterpretations(Concreteness.PARTIAL)); operationCanceledManager.checkCanceled(cancelIndicator); try { var modelSeed = initializer.createModel(problem, builder); @@ -84,7 +85,7 @@ public class SemanticsService extends AbstractCachedService { } return new SemanticsSuccessResult(nodeTrace, partialInterpretation); } catch (RuntimeException e) { - LOG.error("Error while computing semantics", e); + LOG.debug("Error while computing semantics", e); return new SemanticsErrorResult(e.getMessage()); } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningBuilder.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningBuilder.java index f560c74c..3d4c672f 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningBuilder.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningBuilder.java @@ -18,8 +18,17 @@ import tools.refinery.store.reasoning.refinement.StorageRefiner; import tools.refinery.store.reasoning.translator.AnyPartialSymbolTranslator; import tools.refinery.store.representation.Symbol; +import java.util.Collection; +import java.util.List; + @SuppressWarnings("UnusedReturnValue") public interface ReasoningBuilder extends ModelAdapterBuilder { + ReasoningBuilder requiredInterpretations(Collection requiredInterpretations); + + default ReasoningBuilder requiredInterpretations(Concreteness... requiredInterpretations) { + return requiredInterpretations(List.of(requiredInterpretations)); + } + ReasoningBuilder partialSymbol(AnyPartialSymbolTranslator translator); ReasoningBuilder storageRefiner(Symbol symbol, StorageRefiner.Factory refiner); diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningStoreAdapter.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningStoreAdapter.java index 6f9354eb..fe3cc3ea 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningStoreAdapter.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningStoreAdapter.java @@ -7,16 +7,20 @@ package tools.refinery.store.reasoning; import tools.refinery.store.adapter.ModelStoreAdapter; import tools.refinery.store.model.Model; +import tools.refinery.store.reasoning.literal.Concreteness; import tools.refinery.store.reasoning.representation.AnyPartialSymbol; import tools.refinery.store.reasoning.seed.ModelSeed; import java.util.Collection; +import java.util.Set; public interface ReasoningStoreAdapter extends ModelStoreAdapter { Collection getPartialSymbols(); Collection getRefinablePartialSymbols(); + Set getSupportedInterpretations(); + Model createInitialModel(ModelSeed modelSeed); @Override diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java index 579b08dd..2fa744de 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java @@ -48,16 +48,20 @@ class ReasoningAdapterImpl implements ReasoningAdapter { refiners = new HashMap<>(refinerFactories.size()); createRefiners(); - storageRefiners = storeAdapter.createStprageRefiner(model); + storageRefiners = storeAdapter.createStorageRefiner(model); nodeCountInterpretation = model.getInterpretation(NODE_COUNT_SYMBOL); } private void createPartialInterpretations() { + var supportedInterpretations = storeAdapter.getSupportedInterpretations(); int concretenessLength = Concreteness.values().length; var interpretationFactories = storeAdapter.getSymbolInterpreters(); for (int i = 0; i < concretenessLength; i++) { - partialInterpretations[i] = new HashMap<>(interpretationFactories.size()); + var concreteness = Concreteness.values()[i]; + if (supportedInterpretations.contains(concreteness)) { + partialInterpretations[i] = new HashMap<>(interpretationFactories.size()); + } } // Create the partial interpretations in order so that factories may refer to interpretations of symbols // preceding them in the ordered {@code interpretationFactories} map, e.g., for opposite interpretations. @@ -65,9 +69,11 @@ class ReasoningAdapterImpl implements ReasoningAdapter { var partialSymbol = entry.getKey(); var factory = entry.getValue(); for (int i = 0; i < concretenessLength; i++) { - var concreteness = Concreteness.values()[i]; - var interpretation = createPartialInterpretation(concreteness, factory, partialSymbol); - partialInterpretations[i].put(partialSymbol, interpretation); + if (partialInterpretations[i] != null) { + var concreteness = Concreteness.values()[i]; + var interpretation = createPartialInterpretation(concreteness, factory, partialSymbol); + partialInterpretations[i].put(partialSymbol, interpretation); + } } } } @@ -114,6 +120,9 @@ class ReasoningAdapterImpl implements ReasoningAdapter { public PartialInterpretation getPartialInterpretation(Concreteness concreteness, PartialSymbol partialSymbol) { var map = partialInterpretations[concreteness.ordinal()]; + if (map == null) { + throw new IllegalArgumentException("No interpretation for concreteness: " + concreteness); + } var interpretation = map.get(partialSymbol); if (interpretation == null) { throw new IllegalArgumentException("No interpretation for partial symbol: " + partialSymbol); diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningBuilderImpl.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningBuilderImpl.java index 2af84e2d..b4971d2c 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningBuilderImpl.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningBuilderImpl.java @@ -34,6 +34,7 @@ public class ReasoningBuilderImpl extends AbstractModelAdapterBuilder requiredInterpretations = Set.of(Concreteness.values()); private final Map translators = new LinkedHashMap<>(); private final Map> symbolInterpreters = new LinkedHashMap<>(); private final Map> symbolRefiners = @@ -41,6 +42,12 @@ public class ReasoningBuilderImpl extends AbstractModelAdapterBuilder> registeredStorageRefiners = new LinkedHashMap<>(); private final List initializers = new ArrayList<>(); + @Override + public ReasoningBuilder requiredInterpretations(Collection requiredInterpretations) { + this.requiredInterpretations = Set.copyOf(requiredInterpretations); + return this; + } + @Override public ReasoningBuilder partialSymbol(AnyPartialSymbolTranslator translator) { var partialSymbol = translator.getPartialSymbol(); @@ -93,7 +100,7 @@ public class ReasoningBuilderImpl extends AbstractModelAdapterBuilder> getStorageRefiners(ModelStore store) { diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningStoreAdapterImpl.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningStoreAdapterImpl.java index 3dac53ef..8eb5a034 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningStoreAdapterImpl.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningStoreAdapterImpl.java @@ -10,6 +10,7 @@ import tools.refinery.store.model.ModelStore; import tools.refinery.store.query.ModelQueryAdapter; import tools.refinery.store.reasoning.ReasoningStoreAdapter; import tools.refinery.store.reasoning.interpretation.PartialInterpretation; +import tools.refinery.store.reasoning.literal.Concreteness; import tools.refinery.store.reasoning.refinement.PartialInterpretationRefiner; import tools.refinery.store.reasoning.refinement.PartialModelInitializer; import tools.refinery.store.reasoning.refinement.StorageRefiner; @@ -22,20 +23,23 @@ import tools.refinery.store.tuple.Tuple; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; class ReasoningStoreAdapterImpl implements ReasoningStoreAdapter { private final ModelStore store; + private final Set supportedInterpretations; private final Map> symbolInterpreters; private final Map> symbolRefiners; private final Map> storageRefiners; private final List initializers; - ReasoningStoreAdapterImpl(ModelStore store, + ReasoningStoreAdapterImpl(ModelStore store, Set supportedInterpretations, Map> symbolInterpreters, Map> symbolRefiners, Map> storageRefiners, List initializers) { this.store = store; + this.supportedInterpretations = supportedInterpretations; this.symbolInterpreters = symbolInterpreters; this.symbolRefiners = symbolRefiners; this.storageRefiners = storageRefiners; @@ -47,6 +51,11 @@ class ReasoningStoreAdapterImpl implements ReasoningStoreAdapter { return store; } + @Override + public Set getSupportedInterpretations() { + return supportedInterpretations; + } + @Override public Collection getPartialSymbols() { return symbolInterpreters.keySet(); @@ -69,7 +78,7 @@ class ReasoningStoreAdapterImpl implements ReasoningStoreAdapter { return symbolRefiners; } - StorageRefiner[] createStprageRefiner(Model model) { + StorageRefiner[] createStorageRefiner(Model model) { var refiners = new StorageRefiner[storageRefiners.size()]; int i = 0; for (var entry : storageRefiners.entrySet()) { diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/interpretation/PartialInterpretation.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/interpretation/PartialInterpretation.java index 3d3d6056..86ffe751 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/interpretation/PartialInterpretation.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/interpretation/PartialInterpretation.java @@ -6,11 +6,14 @@ package tools.refinery.store.reasoning.interpretation; import tools.refinery.store.map.Cursor; +import tools.refinery.store.model.ModelStoreBuilder; import tools.refinery.store.reasoning.ReasoningAdapter; import tools.refinery.store.reasoning.literal.Concreteness; import tools.refinery.store.reasoning.representation.PartialSymbol; import tools.refinery.store.tuple.Tuple; +import java.util.Set; + public non-sealed interface PartialInterpretation extends AnyPartialInterpretation { @Override PartialSymbol getPartialSymbol(); @@ -19,8 +22,13 @@ public non-sealed interface PartialInterpretation extends AnyPartialInterp Cursor getAll(); + @FunctionalInterface interface Factory { PartialInterpretation create(ReasoningAdapter adapter, Concreteness concreteness, PartialSymbol partialSymbol); + + default void configure(ModelStoreBuilder storeBuilder, Set requiredInterpretations) { + // Nothing to configure by default. + } } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/interpretation/QueryBasedRelationInterpretationFactory.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/interpretation/QueryBasedRelationInterpretationFactory.java index 2535714a..5cdaa185 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/interpretation/QueryBasedRelationInterpretationFactory.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/interpretation/QueryBasedRelationInterpretationFactory.java @@ -6,7 +6,9 @@ package tools.refinery.store.reasoning.interpretation; import tools.refinery.store.map.Cursor; +import tools.refinery.store.model.ModelStoreBuilder; import tools.refinery.store.query.ModelQueryAdapter; +import tools.refinery.store.query.ModelQueryBuilder; import tools.refinery.store.query.dnf.Query; import tools.refinery.store.query.resultset.ResultSet; import tools.refinery.store.reasoning.ReasoningAdapter; @@ -15,6 +17,8 @@ import tools.refinery.store.reasoning.representation.PartialSymbol; import tools.refinery.store.representation.TruthValue; import tools.refinery.store.tuple.Tuple; +import java.util.Set; + public class QueryBasedRelationInterpretationFactory implements PartialInterpretation.Factory { private final Query may; private final Query must; @@ -54,6 +58,17 @@ public class QueryBasedRelationInterpretationFactory implements PartialInterpret } } + @Override + public void configure(ModelStoreBuilder storeBuilder, Set requiredInterpretations) { + var queryBuilder = storeBuilder.getAdapter(ModelQueryBuilder.class); + if (requiredInterpretations.contains(Concreteness.PARTIAL)) { + queryBuilder.queries(may, must); + } + if (requiredInterpretations.contains(Concreteness.CANDIDATE)) { + queryBuilder.queries(candidateMay, candidateMust); + } + } + private static class TwoValuedInterpretation extends AbstractPartialInterpretation { private final ResultSet resultSet; diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/PartialRelationTranslator.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/PartialRelationTranslator.java index 6f9492a3..4600d5a4 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/PartialRelationTranslator.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/PartialRelationTranslator.java @@ -7,7 +7,6 @@ package tools.refinery.store.reasoning.translator; import tools.refinery.store.model.ModelStoreBuilder; import tools.refinery.store.query.Constraint; -import tools.refinery.store.query.ModelQueryBuilder; import tools.refinery.store.query.dnf.Query; import tools.refinery.store.query.dnf.QueryBuilder; import tools.refinery.store.query.dnf.RelationalQuery; @@ -169,7 +168,7 @@ public final class PartialRelationTranslator extends PartialSymbolTranslator implements AnyPartialSymbolTranslator permits PartialRelationTranslator { -- cgit v1.2.3-54-g00ecf From b7a46b805bd7fbb3b21a48a035698ab11fadcb7c Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sat, 19 Aug 2023 14:39:39 +0200 Subject: feat: interruptible VIATRA engine Reduce server load by introducing a timeout for semantics analysis. --- .../language/web/semantics/SemanticsService.java | 129 ++++------------- .../language/web/semantics/SemanticsWorker.java | 133 ++++++++++++++++++ .../web/xtext/server/TransactionExecutor.java | 46 ++++-- .../web/xtext/server/push/PushWebDocument.java | 13 +- .../xtext/server/push/PushWebDocumentProvider.java | 9 +- .../language/web/xtext/servlet/XtextWebSocket.java | 14 +- .../query/viatra/ViatraModelQueryBuilder.java | 9 +- .../internal/ViatraModelQueryAdapterImpl.java | 5 + .../internal/ViatraModelQueryBuilderImpl.java | 10 +- .../internal/ViatraModelQueryStoreAdapterImpl.java | 9 +- .../internal/context/RelationalRuntimeContext.java | 9 ++ .../viatra/runtime/rete/network/ReteContainer.java | 38 ++--- .../viatra/runtime/rete/network/StandardNode.java | 18 +-- .../refinery/viatra/runtime/CancellationToken.java | 13 ++ .../matchers/context/IQueryRuntimeContext.java | 156 +++++++++++---------- 15 files changed, 379 insertions(+), 232 deletions(-) create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java create mode 100644 subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/CancellationToken.java (limited to 'subprojects/language-web/src/main/java/tools') 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 2495430e..39191162 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 @@ -5,8 +5,6 @@ */ 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; @@ -14,43 +12,29 @@ 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 tools.refinery.language.web.xtext.server.push.PushWebDocument; -import java.util.Arrays; -import java.util.List; -import java.util.TreeMap; +import java.util.concurrent.*; @Singleton public class SemanticsService extends AbstractCachedService { private static final Logger LOG = LoggerFactory.getLogger(SemanticsService.class); @Inject - private SemanticsUtils semanticsUtils; + private Provider workerProvider; @Inject - private ValidationService validationService; + private OperationCanceledManager operationCanceledManager; @Inject - private Provider initializerProvider; + private ValidationService validationService; - @Inject - private OperationCanceledManager operationCanceledManager; + private final ExecutorService executorService = Executors.newCachedThreadPool(); @Override public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) { @@ -58,44 +42,42 @@ public class SemanticsService extends AbstractCachedService { if (LOG.isTraceEnabled()) { start = System.currentTimeMillis(); } - Problem problem = getProblem(doc, cancelIndicator); + var problem = getProblem(doc, cancelIndicator); if (problem == null) { return null; } - var initializer = initializerProvider.get(); - var builder = ModelStore.builder() - .with(ViatraModelQueryAdapter.builder()) - .with(ReasoningAdapter.builder() - .requiredInterpretations(Concreteness.PARTIAL)); - operationCanceledManager.checkCanceled(cancelIndicator); + var worker = workerProvider.get(); + worker.setProblem(problem,cancelIndicator); + var future = executorService.submit(worker); + SemanticsResult result = null; 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); - if (LOG.isTraceEnabled()) { - long end = System.currentTimeMillis(); - LOG.trace("Computed semantics for {} ({}) in {}ms", doc.getResourceId(), doc.getStateId(), - end - start); - } - return new SemanticsSuccessResult(nodeTrace, partialInterpretation); - } catch (RuntimeException e) { - LOG.debug("Error while computing semantics", e); - return new SemanticsErrorResult(e.getMessage()); + result = future.get(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + future.cancel(true); + LOG.error("Semantics service interrupted", e); + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + operationCanceledManager.propagateAsErrorIfCancelException(e.getCause()); + throw new IllegalStateException(e); + } catch (TimeoutException e) { + future.cancel(true); + LOG.trace("Semantics service timeout", e); + return new SemanticsErrorResult("Partial interpretation timed out"); } + if (LOG.isTraceEnabled()) { + long end = System.currentTimeMillis(); + LOG.trace("Computed semantics for {} ({}) in {}ms", doc.getResourceId(), doc.getStateId(), + end - start); + } + return result; } @Nullable private Problem getProblem(IXtextWebDocument doc, CancelIndicator cancelIndicator) { - if (!(doc instanceof XtextWebDocument webDoc)) { + if (!(doc instanceof PushWebDocument pushDoc)) { throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc); } - var validationResult = webDoc.getCachedServiceResult(validationService, cancelIndicator, true); + var validationResult = pushDoc.getCachedServiceResult(validationService, cancelIndicator, true); boolean hasError = validationResult.getIssues().stream() .anyMatch(issue -> "error".equals(issue.getSeverity())); if (hasError) { @@ -111,53 +93,4 @@ public class SemanticsService extends AbstractCachedService { } 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/SemanticsWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java new file mode 100644 index 00000000..25589260 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java @@ -0,0 +1,133 @@ +/* + * 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 org.eclipse.xtext.service.OperationCanceledManager; +import org.eclipse.xtext.util.CancelIndicator; +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 tools.refinery.viatra.runtime.CancellationToken; + +import java.util.Arrays; +import java.util.List; +import java.util.TreeMap; +import java.util.concurrent.Callable; + +class SemanticsWorker implements Callable { + private static final Logger LOG = LoggerFactory.getLogger(SemanticsWorker.class); + + @Inject + private SemanticsUtils semanticsUtils; + + @Inject + private OperationCanceledManager operationCanceledManager; + + @Inject + private ModelInitializer initializer; + + private Problem problem; + + private CancellationToken cancellationToken; + + public void setProblem(Problem problem, CancelIndicator parentIndicator) { + this.problem = problem; + cancellationToken = () -> { + if (Thread.interrupted() || parentIndicator.isCanceled()) { + operationCanceledManager.throwOperationCanceledException(); + } + }; + } + + @Override + public SemanticsResult call() { + var builder = ModelStore.builder() + .with(ViatraModelQueryAdapter.builder() + .cancellationToken(cancellationToken)) + .with(ReasoningAdapter.builder() + .requiredInterpretations(Concreteness.PARTIAL)); + cancellationToken.checkCancelled(); + try { + var modelSeed = initializer.createModel(problem, builder); + cancellationToken.checkCancelled(); + var nodeTrace = getNodeTrace(initializer); + cancellationToken.checkCancelled(); + var store = builder.build(); + cancellationToken.checkCancelled(); + var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); + cancellationToken.checkCancelled(); + var partialInterpretation = getPartialInterpretation(initializer, model); + + return new SemanticsSuccessResult(nodeTrace, partialInterpretation); + } catch (RuntimeException e) { + LOG.debug("Error while computing semantics", e); + var message = e.getMessage(); + return new SemanticsErrorResult(message == null ? "Partial interpretation error" : e.getMessage()); + } + } + + 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) { + 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); + cancellationToken.checkCancelled(); + } + 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/xtext/server/TransactionExecutor.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java index 2c0e9329..74456604 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 @@ -42,6 +42,8 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener private final List pendingPushMessages = new ArrayList<>(); + private volatile boolean disposed; + public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { this.session = session; this.resourceServiceProviderRegistry = resourceServiceProviderRegistry; @@ -52,10 +54,13 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener } public void handleRequest(XtextWebRequest request) throws ResponseHandlerException { + if (disposed) { + return; + } var serviceContext = new SimpleServiceContext(session, request.getRequestData()); var ping = serviceContext.getParameter("ping"); if (ping != null) { - responseHandler.onResponse(new XtextWebOkResponse(request, new PongResult(ping))); + onResponse(new XtextWebOkResponse(request, new PongResult(ping))); return; } synchronized (callPendingLock) { @@ -72,23 +77,36 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class); var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this)); var serviceResult = service.getService().apply(); - responseHandler.onResponse(new XtextWebOkResponse(request, serviceResult)); + onResponse(new XtextWebOkResponse(request, serviceResult)); } catch (InvalidRequestException e) { - responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e)); + onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e)); } catch (RuntimeException e) { - responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e)); + onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e)); } finally { - synchronized (callPendingLock) { - for (var message : pendingPushMessages) { - try { - responseHandler.onResponse(message); - } catch (ResponseHandlerException | RuntimeException e) { - LOG.error("Error while flushing push message", e); - } + flushPendingPushMessages(); + } + } + + private void onResponse(XtextWebResponse response) throws ResponseHandlerException { + if (!disposed) { + responseHandler.onResponse(response); + } + } + + private void flushPendingPushMessages() { + synchronized (callPendingLock) { + for (var message : pendingPushMessages) { + if (disposed) { + return; + } + try { + responseHandler.onResponse(message); + } catch (ResponseHandlerException | RuntimeException e) { + LOG.error("Error while flushing push message", e); } - pendingPushMessages.clear(); - callPending = false; } + pendingPushMessages.clear(); + callPending = false; } } @@ -164,10 +182,12 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener @Override public void dispose() { + disposed = true; for (var subscription : subscriptions.values()) { var document = subscription.get(); if (document != null) { document.removePrecomputationListener(this); + document.cancelBackgroundWork(); } } } 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 dfbd4878..1542c694 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 @@ -27,11 +27,11 @@ public class PushWebDocument extends XtextWebDocument { private final Map, IServiceResult> precomputedServices = new HashMap<>(); + private final DocumentSynchronizer synchronizer; + public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) { super(resourceId, synchronizer); - if (resourceId == null) { - throw new IllegalArgumentException("resourceId must not be null"); - } + this.synchronizer = synchronizer; } public void addPrecomputationListener(PrecomputationListener listener) { @@ -63,6 +63,9 @@ public class PushWebDocument extends XtextWebDocument { private void notifyPrecomputationListeners(String serviceName, T result) { var resourceId = getResourceId(); + if (resourceId == null) { + return; + } var stateId = getStateId(); List copyOfListeners; synchronized (precomputationListeners) { @@ -83,4 +86,8 @@ public class PushWebDocument extends XtextWebDocument { } } } + + public void cancelBackgroundWork() { + synchronizer.setCanceled(true); + } } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java index b6f4fb43..ec6204ef 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java @@ -27,12 +27,7 @@ public class PushWebDocumentProvider implements IWebDocumentProvider { @Override public XtextWebDocument get(String resourceId, IServiceContext serviceContext) { - if (resourceId == null) { - return new XtextWebDocument(null, synchronizerProvider.get()); - } else { - // We only need to send push messages if a resourceId is specified. - return new PushWebDocument(resourceId, - serviceContext.getSession().get(DocumentSynchronizer.class, () -> this.synchronizerProvider.get())); - } + return new PushWebDocument(resourceId, + serviceContext.getSession().get(DocumentSynchronizer.class, () -> this.synchronizerProvider.get())); } } 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 043d318c..923fecd6 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 @@ -70,10 +70,11 @@ public class XtextWebSocket implements ResponseHandler { @OnWebSocketError public void onError(Throwable error) { + executor.dispose(); if (webSocketSession == null) { return; } - LOG.error("Internal websocket error in connection from" + webSocketSession.getRemoteSocketAddress(), error); + LOG.error("Internal websocket error in connection from " + webSocketSession.getRemoteSocketAddress(), error); } @OnWebSocketMessage @@ -86,14 +87,18 @@ public class XtextWebSocket implements ResponseHandler { try { request = gson.fromJson(reader, XtextWebRequest.class); } catch (JsonIOException e) { - LOG.error("Cannot read from websocket from" + webSocketSession.getRemoteSocketAddress(), e); + LOG.error("Cannot read from websocket from " + webSocketSession.getRemoteSocketAddress(), e); if (webSocketSession.isOpen()) { + executor.dispose(); webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload", Callback.NOOP); } return; } catch (JsonParseException e) { - LOG.warn("Malformed websocket request from" + webSocketSession.getRemoteSocketAddress(), e); - webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload", Callback.NOOP); + LOG.warn("Malformed websocket request from " + webSocketSession.getRemoteSocketAddress(), e); + if (webSocketSession.isOpen()) { + executor.dispose(); + webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload", Callback.NOOP); + } return; } try { @@ -101,6 +106,7 @@ public class XtextWebSocket implements ResponseHandler { } catch (ResponseHandlerException e) { LOG.warn("Cannot write websocket response", e); if (webSocketSession.isOpen()) { + executor.dispose(); webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response", Callback.NOOP); } } diff --git a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/ViatraModelQueryBuilder.java b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/ViatraModelQueryBuilder.java index d31325f1..6b3be115 100644 --- a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/ViatraModelQueryBuilder.java +++ b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/ViatraModelQueryBuilder.java @@ -5,14 +5,15 @@ */ package tools.refinery.store.query.viatra; -import tools.refinery.viatra.runtime.api.ViatraQueryEngineOptions; -import tools.refinery.viatra.runtime.matchers.backend.IQueryBackendFactory; -import tools.refinery.viatra.runtime.matchers.backend.QueryEvaluationHint; import tools.refinery.store.model.ModelStore; import tools.refinery.store.query.ModelQueryBuilder; import tools.refinery.store.query.dnf.AnyQuery; import tools.refinery.store.query.dnf.Dnf; import tools.refinery.store.query.rewriter.DnfRewriter; +import tools.refinery.viatra.runtime.CancellationToken; +import tools.refinery.viatra.runtime.api.ViatraQueryEngineOptions; +import tools.refinery.viatra.runtime.matchers.backend.IQueryBackendFactory; +import tools.refinery.viatra.runtime.matchers.backend.QueryEvaluationHint; import java.util.Collection; import java.util.function.Function; @@ -29,6 +30,8 @@ public interface ViatraModelQueryBuilder extends ModelQueryBuilder { ViatraModelQueryBuilder searchBackend(IQueryBackendFactory queryBackendFactory); + ViatraModelQueryBuilder cancellationToken(CancellationToken cancellationToken); + @Override default ViatraModelQueryBuilder queries(AnyQuery... queries) { ModelQueryBuilder.super.queries(queries); diff --git a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryAdapterImpl.java b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryAdapterImpl.java index f1209f69..ad754988 100644 --- a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryAdapterImpl.java +++ b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryAdapterImpl.java @@ -18,6 +18,7 @@ import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; import tools.refinery.store.query.viatra.internal.matcher.FunctionalViatraMatcher; import tools.refinery.store.query.viatra.internal.matcher.RawPatternMatcher; import tools.refinery.store.query.viatra.internal.matcher.RelationalViatraMatcher; +import tools.refinery.viatra.runtime.CancellationToken; import tools.refinery.viatra.runtime.api.AdvancedViatraQueryEngine; import tools.refinery.viatra.runtime.api.GenericQueryGroup; import tools.refinery.viatra.runtime.api.IQuerySpecification; @@ -81,6 +82,10 @@ public class ViatraModelQueryAdapterImpl implements ViatraModelQueryAdapter, Mod return storeAdapter; } + public CancellationToken getCancellationToken() { + return storeAdapter.getCancellationToken(); + } + @Override public ResultSet getResultSet(Query query) { var canonicalQuery = storeAdapter.getCanonicalQuery(query); diff --git a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryBuilderImpl.java b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryBuilderImpl.java index cfdc43ba..bb0630f3 100644 --- a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryBuilderImpl.java +++ b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryBuilderImpl.java @@ -17,6 +17,7 @@ import tools.refinery.store.query.viatra.ViatraModelQueryBuilder; import tools.refinery.store.query.viatra.internal.localsearch.FlatCostFunction; import tools.refinery.store.query.viatra.internal.matcher.RawPatternMatcher; import tools.refinery.store.query.viatra.internal.pquery.Dnf2PQuery; +import tools.refinery.viatra.runtime.CancellationToken; import tools.refinery.viatra.runtime.api.IQuerySpecification; import tools.refinery.viatra.runtime.api.ViatraQueryEngineOptions; import tools.refinery.viatra.runtime.localsearch.matcher.integration.LocalSearchGenericBackendFactory; @@ -35,6 +36,7 @@ public class ViatraModelQueryBuilderImpl extends AbstractModelAdapterBuilder queries = new LinkedHashSet<>(); @@ -84,6 +86,12 @@ public class ViatraModelQueryBuilderImpl extends AbstractModelAdapterBuilder queries) { checkNotConfigured(); @@ -136,7 +144,7 @@ public class ViatraModelQueryBuilderImpl extends AbstractModelAdapterBuilder> querySpecifications; private final Set vacuousQueries; private final Set allQueries; + private final CancellationToken cancellationToken; ViatraModelQueryStoreAdapterImpl(ModelStore store, ViatraQueryEngineOptions engineOptions, Map inputKeys, Map canonicalQueryMap, Map> querySpecifications, - Set vacuousQueries) { + Set vacuousQueries, CancellationToken cancellationToken) { this.store = store; this.engineOptions = engineOptions; this.inputKeys = inputKeys; this.canonicalQueryMap = canonicalQueryMap; this.querySpecifications = querySpecifications; this.vacuousQueries = vacuousQueries; + this.cancellationToken = cancellationToken; var mutableAllQueries = new LinkedHashSet(querySpecifications.size() + vacuousQueries.size()); mutableAllQueries.addAll(querySpecifications.keySet()); mutableAllQueries.addAll(vacuousQueries); @@ -62,6 +65,10 @@ public class ViatraModelQueryStoreAdapterImpl implements ViatraModelQueryStoreAd return allQueries; } + public CancellationToken getCancellationToken() { + return cancellationToken; + } + @Override public Query getCanonicalQuery(Query query) { // We know that canonical forms of queries do not change output types. diff --git a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/context/RelationalRuntimeContext.java b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/context/RelationalRuntimeContext.java index d1fa5239..dadab5dd 100644 --- a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/context/RelationalRuntimeContext.java +++ b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/context/RelationalRuntimeContext.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.viatra.internal.context; +import tools.refinery.viatra.runtime.CancellationToken; import tools.refinery.viatra.runtime.matchers.context.*; import tools.refinery.viatra.runtime.matchers.tuple.ITuple; import tools.refinery.viatra.runtime.matchers.tuple.Tuple; @@ -32,10 +33,13 @@ public class RelationalRuntimeContext implements IQueryRuntimeContext { private final Model model; + private final CancellationToken cancellationToken; + RelationalRuntimeContext(ViatraModelQueryAdapterImpl adapter) { model = adapter.getModel(); metaContext = new RelationalQueryMetaContext(adapter.getStoreAdapter().getInputKeys()); modelUpdateListener = new ModelUpdateListener(adapter); + cancellationToken = adapter.getCancellationToken(); } @Override @@ -192,4 +196,9 @@ public class RelationalRuntimeContext implements IQueryRuntimeContext { public void executeAfterTraversal(Runnable runnable) { runnable.run(); } + + @Override + public CancellationToken getCancellationToken() { + return cancellationToken; + } } diff --git a/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/ReteContainer.java b/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/ReteContainer.java index 16e290fd..79e0526d 100644 --- a/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/ReteContainer.java +++ b/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/ReteContainer.java @@ -1,26 +1,17 @@ /******************************************************************************* * Copyright (c) 2004-2008 Gabor Bergmann and Daniel Varro + * Copyright (c) 2023 The Refinery Authors * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-v20.html. - * + * * SPDX-License-Identifier: EPL-2.0 *******************************************************************************/ package tools.refinery.viatra.runtime.rete.network; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Deque; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - import org.apache.log4j.Logger; +import tools.refinery.viatra.runtime.CancellationToken; import tools.refinery.viatra.runtime.matchers.context.IQueryBackendContext; import tools.refinery.viatra.runtime.matchers.tuple.Tuple; import tools.refinery.viatra.runtime.matchers.util.Clearable; @@ -42,6 +33,9 @@ import tools.refinery.viatra.runtime.rete.single.SingleInputNode; import tools.refinery.viatra.runtime.rete.single.TrimmerNode; import tools.refinery.viatra.runtime.rete.util.Options; +import java.util.*; +import java.util.function.Function; + /** * @author Gabor Bergmann * @@ -79,6 +73,8 @@ public final class ReteContainer { protected final TimelyConfiguration timelyConfiguration; + private final CancellationToken cancellationToken; + /** * @param threaded * false if operating in a single-threaded environment @@ -88,6 +84,7 @@ public final class ReteContainer { this.network = network; this.backendContext = network.getEngine().getBackendContext(); this.timelyConfiguration = network.getEngine().getTimelyConfiguration(); + cancellationToken = backendContext.getRuntimeContext().getCancellationToken(); this.delayedCommandQueue = new LinkedHashSet(); this.delayedCommandBuffer = new LinkedHashSet(); @@ -395,10 +392,10 @@ public final class ReteContainer { /** * Retrieves a safe copy of the contents of a supplier. - * + * *

Note that there may be multiple copies of a Tuple in case of a {@link TrimmerNode}, so the result is not always a set. - * - * @param flush if true, a flush is performed before pulling the contents + * + * @param flush if true, a flush is performed before pulling the contents * @since 2.3 */ public Collection pullContents(final Supplier supplier, final boolean flush) { @@ -424,7 +421,7 @@ public final class ReteContainer { /** * Retrieves the contents of a SingleInputNode's parentage. - * + * * @since 2.3 */ public Collection pullPropagatedContents(final SingleInputNode supplier, final boolean flush) { @@ -438,7 +435,7 @@ public final class ReteContainer { /** * Retrieves the timestamp-aware contents of a SingleInputNode's parentage. - * + * * @since 2.3 */ public Map> pullPropagatedContentsWithTimestamp(final SingleInputNode supplier, @@ -541,7 +538,7 @@ public final class ReteContainer { /** * Sends out all pending messages to their receivers. The delivery is governed by the communication tracker. - * + * * @since 1.6 */ public void deliverMessagesSingleThreaded() { @@ -620,7 +617,7 @@ public final class ReteContainer { /** * Returns an addressed node at this container. - * + * * @pre: address.container == this, e.g. address MUST be local * @throws IllegalArgumentException * if address is non-local @@ -726,4 +723,7 @@ public final class ReteContainer { return network.getInputConnector(); } + public void checkCancelled() { + cancellationToken.checkCancelled(); + } } diff --git a/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/StandardNode.java b/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/StandardNode.java index e7ec36dc..7dc7c4bc 100644 --- a/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/StandardNode.java +++ b/subprojects/viatra-runtime-rete/src/main/java/tools/refinery/viatra/runtime/rete/network/StandardNode.java @@ -1,19 +1,15 @@ /******************************************************************************* * Copyright (c) 2004-2008 Gabor Bergmann and Daniel Varro + * Copyright (c) 2023 The Refinery Authors * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-v20.html. - * + * * SPDX-License-Identifier: EPL-2.0 *******************************************************************************/ package tools.refinery.viatra.runtime.rete.network; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import tools.refinery.viatra.runtime.matchers.tuple.Tuple; import tools.refinery.viatra.runtime.matchers.tuple.TupleMask; import tools.refinery.viatra.runtime.matchers.util.CollectionsFactory; @@ -24,11 +20,16 @@ import tools.refinery.viatra.runtime.rete.network.communication.Timestamp; import tools.refinery.viatra.runtime.rete.network.mailbox.Mailbox; import tools.refinery.viatra.runtime.rete.traceability.TraceInfo; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + /** * Base implementation for a supplier node. - * + * * @author Gabor Bergmann - * + * */ public abstract class StandardNode extends BaseNode implements Supplier, NetworkStructureChangeSensitiveNode { protected final List children = CollectionsFactory.createObserverList(); @@ -45,6 +46,7 @@ public abstract class StandardNode extends BaseNode implements Supplier, Network * @since 2.4 */ protected void propagateUpdate(final Direction direction, final Tuple updateElement, final Timestamp timestamp) { + reteContainer.checkCancelled(); for (final Mailbox childMailbox : childMailboxes) { childMailbox.postMessage(direction, updateElement, timestamp); } diff --git a/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/CancellationToken.java b/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/CancellationToken.java new file mode 100644 index 00000000..a2ae41e3 --- /dev/null +++ b/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/CancellationToken.java @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.viatra.runtime; + +@FunctionalInterface +public interface CancellationToken { + CancellationToken NONE = () -> {}; + + void checkCancelled(); +} diff --git a/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/context/IQueryRuntimeContext.java b/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/context/IQueryRuntimeContext.java index c2e90614..61359c1b 100644 --- a/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/context/IQueryRuntimeContext.java +++ b/subprojects/viatra-runtime/src/main/java/tools/refinery/viatra/runtime/matchers/context/IQueryRuntimeContext.java @@ -1,82 +1,84 @@ /******************************************************************************* * Copyright (c) 2010-2015, Bergmann Gabor, Istvan Rath and Daniel Varro + * Copyright (c) 2023 The Refinery Authors * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-v20.html. - * + * * SPDX-License-Identifier: EPL-2.0 *******************************************************************************/ package tools.refinery.viatra.runtime.matchers.context; -import java.lang.reflect.InvocationTargetException; -import java.util.Optional; -import java.util.concurrent.Callable; - +import tools.refinery.viatra.runtime.CancellationToken; import tools.refinery.viatra.runtime.matchers.planning.helpers.StatisticsHelper; import tools.refinery.viatra.runtime.matchers.tuple.ITuple; import tools.refinery.viatra.runtime.matchers.tuple.Tuple; import tools.refinery.viatra.runtime.matchers.tuple.TupleMask; import tools.refinery.viatra.runtime.matchers.util.Accuracy; +import java.lang.reflect.InvocationTargetException; +import java.util.Optional; +import java.util.concurrent.Callable; + /** * Provides instance model information (relations corresponding to input keys) to query evaluator backends at runtime. * Implementors shall extend {@link AbstractQueryRuntimeContext} instead directly this interface. - * + * * @author Bergmann Gabor * @noimplement This interface is not intended to be implemented by clients. Extend {@link AbstractQueryRuntimeContext} instead. */ public interface IQueryRuntimeContext { - /** + /** * Provides metamodel-specific info independent of the runtime instance model. */ public IQueryMetaContext getMetaContext(); - - + + /** * The given callable will be executed, and all model traversals will be delayed until the execution is done. If * there are any outstanding information to be read from the model, a single coalesced model traversal will * initialize the caches and deliver the notifications. - * + * *

Calls may be nested. A single coalesced traversal will happen at the end of the outermost call. - * - *

Caution: results returned by the runtime context may be incomplete during the coalescing period, to be corrected by notifications sent during the final coalesced traversal. - * For example, if a certain input key is not cached yet, an empty relation may be reported during callable.call(); the cache will be constructed after the call terminates and notifications will deliver the entire content of the relation. + * + *

Caution: results returned by the runtime context may be incomplete during the coalescing period, to be corrected by notifications sent during the final coalesced traversal. + * For example, if a certain input key is not cached yet, an empty relation may be reported during callable.call(); the cache will be constructed after the call terminates and notifications will deliver the entire content of the relation. * Non-incremental query backends should therefore never enumerate input keys while coalesced (verify using {@link #isCoalescing()}). - * + * * @param callable */ - public abstract V coalesceTraversals(Callable callable) throws InvocationTargetException; + public abstract V coalesceTraversals(Callable callable) throws InvocationTargetException; /** * @return true iff currently within a coalescing section (i.e. within the callable of a call to {@link #coalesceTraversals(Callable)}). */ public boolean isCoalescing(); - + /** * Returns true if index is available for the given key providing the given service. * @throws IllegalArgumentException if key is not enumerable or an unknown type, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @since 1.4 */ public boolean isIndexed(IInputKey key, IndexingService service); - + /** - * If the given (enumerable) input key is not yet indexed, the model will be traversed - * (after the end of the outermost coalescing block, see {@link IQueryRuntimeContext#coalesceTraversals(Callable)}) + * If the given (enumerable) input key is not yet indexed, the model will be traversed + * (after the end of the outermost coalescing block, see {@link IQueryRuntimeContext#coalesceTraversals(Callable)}) * so that the index can be built. It is possible that the base indexer will select a higher indexing level merging * multiple indexing requests to an appropriate level. - * + * *

Postcondition: After invoking this method, {@link #getIndexed(IInputKey, IndexingService)} for the same key * and service will be guaranteed to return the requested or a highing indexing level as soon as {@link #isCoalescing()} first returns false. - * + * *

Precondition: the given key is enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @throws IllegalArgumentException if key is not enumerable or an unknown type, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @since 1.4 */ public void ensureIndexed(IInputKey key, IndexingService service); - + /** * Returns the number of tuples in the extensional relation identified by the input key seeded with the given mask and tuple. - * - * @param key an input key + * + * @param key an input key * @param seedMask * a mask that extracts those parameters of the input key (from the entire parameter list) that should be * bound to a fixed value; must not be null. Note: any given index must occur at most once in seedMask. @@ -84,59 +86,59 @@ public interface IQueryRuntimeContext { * the tuple of fixed values restricting the match set to be considered, in the same order as given in * parameterSeedMask, so that for each considered match tuple, * projectedParameterSeed.equals(parameterSeedMask.transform(match)) should hold. Must not be null. - * + * * @return the number of tuples in the model for the given key and seed - * + * *

Precondition: the given key is enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @throws IllegalArgumentException if key is not enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @since 1.7 */ public int countTuples(IInputKey key, TupleMask seedMask, ITuple seed); - - + + /** * Gives an estimate of the number of different groups the tuples of the given relation are projected into by the given mask * (e.g. for an identity mask, this means the full relation size). The estimate must meet the required accuracy. - * - *

Must accept any input key, even non-enumerables or those not recognized by this runtime context. - * If there is insufficient information to provide an answer up to the required precision, {@link Optional#empty()} is returned. - * + * + *

Must accept any input key, even non-enumerables or those not recognized by this runtime context. + * If there is insufficient information to provide an answer up to the required precision, {@link Optional#empty()} is returned. + * *

PRE: {@link TupleMask#isNonrepeating()} must hold for the group mask. - * + * * @return if available, an estimate of the cardinality of the projection of the given extensional relation, with the desired accuracy. - * + * * @since 2.1 */ public Optional estimateCardinality(IInputKey key, TupleMask groupMask, Accuracy requiredAccuracy); - - + + /** * Gives an estimate of the average size of different groups the tuples of the given relation are projected into by the given mask - * (e.g. for an identity mask, this means 1, while for an empty mask, the result is the full relation size). + * (e.g. for an identity mask, this means 1, while for an empty mask, the result is the full relation size). * The estimate must meet the required accuracy. - * - *

Must accept any input key, even non-enumerables or those not recognized by this runtime context. + * + *

Must accept any input key, even non-enumerables or those not recognized by this runtime context. * If there is insufficient information to provide an answer up to the required precision, {@link Optional#empty()} may be returned. - * + * *

For an empty relation, zero is acceptable as an exact answer. - * + * *

PRE: {@link TupleMask#isNonrepeating()} must hold for the group mask. - * + * * @return if available, an estimate of the average size of each projection group of the given extensional relation, with the desired accuracy. - * + * * @since 2.1 */ public default Optional estimateAverageBucketSize(IInputKey key, TupleMask groupMask, Accuracy requiredAccuracy) { if (key.isEnumerable()) { - return StatisticsHelper.estimateAverageBucketSize(groupMask, requiredAccuracy, + return StatisticsHelper.estimateAverageBucketSize(groupMask, requiredAccuracy, (mask, accuracy) -> this.estimateCardinality(key, mask, accuracy)); } else return groupMask.isIdentity() ? Optional.of(1.0) : Optional.empty(); } - - + + /** * Returns the tuples in the extensional relation identified by the input key, optionally seeded with the given tuple. - * + * * @param key an input key * @param seedMask * a mask that extracts those parameters of the input key (from the entire parameter list) that should be @@ -144,23 +146,23 @@ public interface IQueryRuntimeContext { * @param seed * the tuple of fixed values restricting the match set to be considered, in the same order as given in * parameterSeedMask, so that for each considered match tuple, - * projectedParameterSeed.equals(parameterSeedMask.transform(match)) should hold. Must not be null. + * projectedParameterSeed.equals(parameterSeedMask.transform(match)) should hold. Must not be null. * @return the tuples in the model for the given key and seed - * + * *

Precondition: the given key is enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @throws IllegalArgumentException if key is not enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @since 1.7 */ public Iterable enumerateTuples(IInputKey key, TupleMask seedMask, ITuple seed); - + /** * Simpler form of {@link #enumerateTuples(IInputKey, TupleMask, Tuple)} in the case where all values of the tuples * are bound by the seed except for one. - * + * *

* Selects the tuples in the extensional relation identified by the input key, optionally seeded with the given * tuple, and then returns the single value from each tuple which is not bound by the ssed mask. - * + * * @param key * an input key * @param seedMask @@ -172,7 +174,7 @@ public interface IQueryRuntimeContext { * parameterSeedMask, so that for each considered match tuple, * projectedParameterSeed.equals(parameterSeedMask.transform(match)) should hold. Must not be null. * @return the objects in the model for the given key and seed - * + * *

* Precondition: the given key is enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @throws IllegalArgumentException @@ -180,17 +182,17 @@ public interface IQueryRuntimeContext { * @since 1.7 */ public Iterable enumerateValues(IInputKey key, TupleMask seedMask, ITuple seed); - + /** * Simpler form of {@link #enumerateTuples(IInputKey, TupleMask, Tuple)} in the case where all values of the tuples * are bound by the seed. - * + * *

* Returns whether the given tuple is in the extensional relation identified by the input key. - * + * *

* Note: this call works for non-enumerable input keys as well. - * + * * @param key * an input key * @param seed @@ -202,31 +204,31 @@ public interface IQueryRuntimeContext { */ public boolean containsTuple(IInputKey key, ITuple seed); - + /** * Subscribes for updates in the extensional relation identified by the input key, optionally seeded with the given tuple. - *

This should be called after invoking - * + *

This should be called after invoking + * * @param key an input key - * @param seed can be null or a tuple with matching arity; - * if non-null, only those updates in the model are notified about - * that match the seed at positions where the seed is non-null. + * @param seed can be null or a tuple with matching arity; + * if non-null, only those updates in the model are notified about + * that match the seed at positions where the seed is non-null. * @param listener will be notified of future changes - * + * *

Precondition: the given key is enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @throws IllegalArgumentException if key is not enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. */ public void addUpdateListener(IInputKey key, Tuple seed, IQueryRuntimeContextListener listener); - + /** * Unsubscribes from updates in the extensional relation identified by the input key, optionally seeded with the given tuple. - * + * * @param key an input key - * @param seed can be null or a tuple with matching arity; - * if non-null, only those updates in the model are notified about - * that match the seed at positions where the seed is non-null. + * @param seed can be null or a tuple with matching arity; + * if non-null, only those updates in the model are notified about + * that match the seed at positions where the seed is non-null. * @param listener will no longer be notified of future changes - * + * *

Precondition: the given key is enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. * @throws IllegalArgumentException if key is not enumerable, see {@link IQueryMetaContext#isEnumerable(IInputKey)}. */ @@ -234,16 +236,16 @@ public interface IQueryRuntimeContext { /* TODO: uniqueness */ - + /** - * Wraps the external element into the internal representation that is to be used by the query backend + * Wraps the external element into the internal representation that is to be used by the query backend *

model element -> internal object. *

null must be mapped to null. */ public Object wrapElement(Object externalElement); /** - * Unwraps the internal representation of the element into its original form + * Unwraps the internal representation of the element into its original form *

internal object -> model element *

null must be mapped to null. */ @@ -269,13 +271,17 @@ public interface IQueryRuntimeContext { * @since 1.4 */ public void ensureWildcardIndexing(IndexingService service); - + /** * Execute the given runnable after traversal. It is guaranteed that the runnable is executed as soon as * the indexing is finished. The callback is executed only once, then is removed from the callback queue. * @param traversalCallback - * @throws InvocationTargetException + * @throws InvocationTargetException * @since 1.4 */ public void executeAfterTraversal(Runnable runnable) throws InvocationTargetException; + + default CancellationToken getCancellationToken() { + return CancellationToken.NONE; + } } -- cgit v1.2.3-54-g00ecf From a3f1e6872f4f768d14899a1e70bbdc14f32e478d Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 20 Aug 2023 19:41:32 +0200 Subject: feat: improve semantics error reporting Also makes model seeds cancellable to reduce server load during semantic analysis. --- .../src/editor/AnalysisErrorNotification.tsx | 74 +++++++++++++ subprojects/frontend/src/editor/AnimatedButton.tsx | 9 +- subprojects/frontend/src/editor/EditorButtons.tsx | 6 +- subprojects/frontend/src/editor/EditorErrors.tsx | 93 ++++++++++++++++ subprojects/frontend/src/editor/EditorPane.tsx | 2 + subprojects/frontend/src/editor/EditorStore.ts | 39 ++++--- subprojects/frontend/src/editor/EditorTheme.ts | 4 +- subprojects/frontend/src/editor/GenerateButton.tsx | 48 +++++--- subprojects/frontend/src/xtext/SemanticsService.ts | 28 +++++ subprojects/frontend/src/xtext/UpdateService.ts | 2 + .../frontend/src/xtext/ValidationService.ts | 44 ++++++-- subprojects/frontend/src/xtext/XtextClient.ts | 7 +- .../frontend/src/xtext/xtextServiceResults.ts | 7 ++ .../language/semantics/model/ModelInitializer.java | 121 +++++++++++++++------ .../language/semantics/model/TracedException.java | 51 +++++++++ .../language/web/semantics/CancellableSeed.java | 99 +++++++++++++++++ .../web/semantics/SemanticsErrorResult.java | 9 -- .../semantics/SemanticsInternalErrorResult.java | 9 ++ .../web/semantics/SemanticsIssuesResult.java | 13 +++ .../language/web/semantics/SemanticsResult.java | 3 +- .../language/web/semantics/SemanticsService.java | 43 ++++++-- .../language/web/semantics/SemanticsWorker.java | 44 ++++++-- .../web/xtext/server/push/PushWebDocument.java | 3 +- .../ProblemWebSocketServletIntegrationTest.java | 9 +- .../web/xtext/servlet/TransactionExecutorTest.java | 8 +- .../store/query/InvalidQueryException.java | 23 ++++ .../store/query/dnf/ClausePostProcessor.java | 9 +- .../java/tools/refinery/store/query/dnf/Dnf.java | 3 +- .../tools/refinery/store/query/dnf/DnfBuilder.java | 88 +-------------- .../refinery/store/query/dnf/DnfPostProcessor.java | 112 +++++++++++++++++++ .../store/query/dnf/FunctionalDependency.java | 4 +- .../refinery/store/query/dnf/FunctionalQuery.java | 5 +- .../store/query/dnf/InvalidClauseException.java | 35 ++++++ .../refinery/store/query/dnf/RelationalQuery.java | 3 +- .../store/query/literal/AbstractCallLiteral.java | 5 +- .../store/query/literal/AbstractCountLiteral.java | 5 +- .../store/query/literal/AggregationLiteral.java | 13 ++- .../store/query/literal/AssignLiteral.java | 7 +- .../refinery/store/query/literal/CallLiteral.java | 7 +- .../refinery/store/query/literal/CallPolarity.java | 4 +- .../refinery/store/query/literal/CheckLiteral.java | 5 +- .../store/query/literal/EquivalenceLiteral.java | 3 +- .../literal/RepresentativeElectionLiteral.java | 9 +- .../refinery/store/query/term/AnyDataVariable.java | 3 +- .../refinery/store/query/term/BinaryTerm.java | 5 +- .../refinery/store/query/term/ConstantTerm.java | 3 +- .../refinery/store/query/term/DataVariable.java | 5 +- .../refinery/store/query/term/NodeVariable.java | 3 +- .../store/query/term/ParameterDirection.java | 4 +- .../tools/refinery/store/query/term/UnaryTerm.java | 3 +- .../refinery/store/query/view/FilteredView.java | 3 +- .../store/query/dnf/DnfToDefinitionStringTest.java | 4 +- .../store/query/dnf/TopologicalSortTest.java | 7 +- .../store/query/dnf/VariableDirectionTest.java | 10 +- .../query/literal/AggregationLiteralTest.java | 13 ++- .../store/reasoning/literal/ModalConstraint.java | 3 +- .../store/reasoning/literal/PartialLiterals.java | 3 +- .../refinery/store/reasoning/seed/ModelSeed.java | 5 + .../reasoning/translator/TranslationException.java | 35 ++++++ .../translator/containment/ContainmentInfo.java | 7 +- .../metamodel/ContainedTypeHierarchyBuilder.java | 3 +- .../translator/metamodel/MetamodelBuilder.java | 32 +++--- .../multiobject/MultiObjectInitializer.java | 31 +++--- .../multiplicity/ConstrainedMultiplicity.java | 7 +- .../InvalidMultiplicityErrorTranslator.java | 5 +- .../opposite/OppositeRelationTranslator.java | 11 ++ .../translator/predicate/PredicateTranslator.java | 5 +- .../translator/typehierarchy/TypeHierarchy.java | 8 +- .../typehierarchy/TypeHierarchyBuilder.java | 9 +- .../translator/metamodel/MetamodelBuilderTest.java | 7 +- .../typehierarchy/TypeHierarchyTest.java | 3 +- 71 files changed, 1044 insertions(+), 308 deletions(-) create mode 100644 subprojects/frontend/src/editor/AnalysisErrorNotification.tsx create mode 100644 subprojects/frontend/src/editor/EditorErrors.tsx create mode 100644 subprojects/frontend/src/xtext/SemanticsService.ts create mode 100644 subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/TracedException.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java delete 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/SemanticsInternalErrorResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java create mode 100644 subprojects/store-query/src/main/java/tools/refinery/store/query/InvalidQueryException.java create mode 100644 subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfPostProcessor.java create mode 100644 subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/InvalidClauseException.java create mode 100644 subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/TranslationException.java (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx b/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx new file mode 100644 index 00000000..591a3600 --- /dev/null +++ b/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { reaction } from 'mobx'; +import { type SnackbarKey, useSnackbar } from 'notistack'; +import { useEffect, useState } from 'react'; + +import type EditorStore from './EditorStore'; + +function MessageObserver({ + editorStore, +}: { + editorStore: EditorStore; +}): React.ReactNode { + const [message, setMessage] = useState( + editorStore.delayedErrors.semanticsError ?? '', + ); + // Instead of making this component an `observer`, + // we only update the message is one is present to make sure that the + // disappear animation has a chance to complete. + useEffect( + () => + reaction( + () => editorStore.delayedErrors.semanticsError, + (newMessage) => { + if (newMessage !== undefined) { + setMessage(newMessage); + } + }, + { fireImmediately: false }, + ), + [editorStore], + ); + return message; +} + +export default function AnalysisErrorNotification({ + editorStore, +}: { + editorStore: EditorStore; +}): null { + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + useEffect(() => { + let key: SnackbarKey | undefined; + const disposer = reaction( + () => editorStore.delayedErrors.semanticsError !== undefined, + (hasError) => { + if (hasError) { + if (key === undefined) { + key = enqueueSnackbar({ + message: , + variant: 'error', + persist: true, + }); + } + } else if (key !== undefined) { + closeSnackbar(key); + key = undefined; + } + }, + { fireImmediately: true }, + ); + return () => { + disposer(); + if (key !== undefined) { + closeSnackbar(key); + } + }; + }, [editorStore, enqueueSnackbar, closeSnackbar]); + return null; +} diff --git a/subprojects/frontend/src/editor/AnimatedButton.tsx b/subprojects/frontend/src/editor/AnimatedButton.tsx index dbbda618..24ec69be 100644 --- a/subprojects/frontend/src/editor/AnimatedButton.tsx +++ b/subprojects/frontend/src/editor/AnimatedButton.tsx @@ -48,7 +48,7 @@ export default function AnimatedButton({ onClick?: () => void; color: 'error' | 'warning' | 'primary' | 'inherit'; disabled?: boolean; - startIcon: JSX.Element; + startIcon?: JSX.Element; sx?: SxProps | undefined; children?: ReactNode; }): JSX.Element { @@ -79,7 +79,11 @@ export default function AnimatedButton({ className="rounded shaded" disabled={disabled ?? false} startIcon={startIcon} - width={width === undefined ? 'auto' : `calc(${width} + 50px)`} + width={ + width === undefined + ? 'auto' + : `calc(${width} + ${startIcon === undefined ? 28 : 50}px)` + } > ; + return ; case 'warning': return ; case 'info': @@ -95,7 +95,7 @@ export default observer(function EditorButtons({ })} value="show-lint-panel" > - {getLintIcon(editorStore?.highestDiagnosticLevel)} + {getLintIcon(editorStore?.delayedErrors?.highestDiagnosticLevel)} + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { Diagnostic } from '@codemirror/lint'; +import { type IReactionDisposer, makeAutoObservable, reaction } from 'mobx'; + +import type EditorStore from './EditorStore'; + +const HYSTERESIS_TIME_MS = 250; + +export interface State { + analyzing: boolean; + errorCount: number; + warningCount: number; + infoCount: number; + semanticsError: string | undefined; +} + +export default class EditorErrors implements State { + private readonly disposer: IReactionDisposer; + + private timer: number | undefined; + + analyzing = false; + + errorCount = 0; + + warningCount = 0; + + infoCount = 0; + + semanticsError: string | undefined; + + constructor(private readonly store: EditorStore) { + this.updateImmediately(this.getNextState()); + makeAutoObservable(this, { + disposer: false, + timer: false, + }); + this.disposer = reaction( + () => this.getNextState(), + (nextState) => { + if (this.timer !== undefined) { + clearTimeout(this.timer); + this.timer = undefined; + } + if (nextState.analyzing) { + this.timer = setTimeout( + () => this.updateImmediately(nextState), + HYSTERESIS_TIME_MS, + ); + } else { + this.updateImmediately(nextState); + } + }, + { fireImmediately: true }, + ); + } + + get highestDiagnosticLevel(): Diagnostic['severity'] | undefined { + if (this.errorCount > 0) { + return 'error'; + } + if (this.warningCount > 0) { + return 'warning'; + } + if (this.infoCount > 0) { + return 'info'; + } + return undefined; + } + + private getNextState(): State { + return { + analyzing: this.store.analyzing, + errorCount: this.store.errorCount, + warningCount: this.store.warningCount, + infoCount: this.store.infoCount, + semanticsError: this.store.semanticsError, + }; + } + + private updateImmediately(nextState: State) { + Object.assign(this, nextState); + } + + dispose() { + this.disposer(); + } +} diff --git a/subprojects/frontend/src/editor/EditorPane.tsx b/subprojects/frontend/src/editor/EditorPane.tsx index c9f86496..1125a0ec 100644 --- a/subprojects/frontend/src/editor/EditorPane.tsx +++ b/subprojects/frontend/src/editor/EditorPane.tsx @@ -13,6 +13,7 @@ import { useState } from 'react'; import { useRootStore } from '../RootStoreProvider'; +import AnalysisErrorNotification from './AnalysisErrorNotification'; import ConnectionStatusNotification from './ConnectionStatusNotification'; import EditorArea from './EditorArea'; import EditorButtons from './EditorButtons'; @@ -48,6 +49,7 @@ export default observer(function EditorPane(): JSX.Element { ) : ( <> + diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index c79f6ec1..563725bb 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts @@ -29,6 +29,7 @@ import type PWAStore from '../PWAStore'; import getLogger from '../utils/getLogger'; import type XtextClient from '../xtext/XtextClient'; +import EditorErrors from './EditorErrors'; import LintPanelStore from './LintPanelStore'; import SearchPanelStore from './SearchPanelStore'; import createEditorState from './createEditorState'; @@ -54,15 +55,22 @@ export default class EditorStore { readonly lintPanel: LintPanelStore; + readonly delayedErrors: EditorErrors; + showLineNumbers = false; disposed = false; + analyzing = false; + + semanticsError: string | undefined; + semantics: unknown = {}; constructor(initialValue: string, pwaStore: PWAStore) { this.id = nanoid(); this.state = createEditorState(initialValue, this); + this.delayedErrors = new EditorErrors(this); this.searchPanel = new SearchPanelStore(this); this.lintPanel = new LintPanelStore(this); (async () => { @@ -82,6 +90,7 @@ export default class EditorStore { state: observable.ref, client: observable.ref, view: observable.ref, + semantics: observable.ref, searchPanel: false, lintPanel: false, contentAssist: false, @@ -215,19 +224,6 @@ export default class EditorStore { this.doCommand(nextDiagnostic); } - get highestDiagnosticLevel(): Diagnostic['severity'] | undefined { - if (this.errorCount > 0) { - return 'error'; - } - if (this.warningCount > 0) { - return 'warning'; - } - if (this.infoCount > 0) { - return 'info'; - } - return undefined; - } - updateSemanticHighlighting(ranges: IHighlightRange[]): void { this.dispatch(setSemanticHighlighting(ranges)); } @@ -284,12 +280,29 @@ export default class EditorStore { return true; } + analysisStarted() { + this.analyzing = true; + } + + analysisCompleted(semanticAnalysisSkipped = false) { + this.analyzing = false; + if (semanticAnalysisSkipped) { + this.semanticsError = undefined; + } + } + + setSemanticsError(semanticsError: string) { + this.semanticsError = semanticsError; + } + setSemantics(semantics: unknown) { + this.semanticsError = undefined; this.semantics = semantics; } dispose(): void { this.client?.dispose(); + this.delayedErrors.dispose(); this.disposed = true; } } diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index 4afb93e6..dd551a52 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import errorSVG from '@material-icons/svg/svg/error/baseline.svg?raw'; +import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; @@ -331,7 +331,7 @@ export default styled('div', { '.cm-lintRange-active': { background: theme.palette.highlight.activeLintRange, }, - ...lintSeverityStyle('error', errorSVG, 120), + ...lintSeverityStyle('error', cancelSVG, 120), ...lintSeverityStyle('warning', warningSVG, 110), ...lintSeverityStyle('info', infoSVG, 100), }; diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 3837ef8e..5bac0464 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx @@ -4,10 +4,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -import DangerousOutlinedIcon from '@mui/icons-material/DangerousOutlined'; +import CancelIcon from '@mui/icons-material/Cancel'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; -import Button from '@mui/material/Button'; -import type { SxProps, Theme } from '@mui/material/styles'; import { observer } from 'mobx-react-lite'; import AnimatedButton from './AnimatedButton'; @@ -18,26 +16,45 @@ const GENERATE_LABEL = 'Generate'; const GenerateButton = observer(function GenerateButton({ editorStore, hideWarnings, - sx, }: { editorStore: EditorStore | undefined; hideWarnings?: boolean | undefined; - sx?: SxProps | undefined; }): JSX.Element { if (editorStore === undefined) { return ( - + + ); + } + + const { analyzing, errorCount, warningCount, semanticsError } = + editorStore.delayedErrors; + + if (analyzing) { + return ( + + Analyzing… + ); } - const { errorCount, warningCount } = editorStore; + if (semanticsError !== undefined && editorStore.opened) { + return ( + } + sx={(theme) => ({ + '&.Mui-disabled': { + color: `${theme.palette.error.main} !important`, + }, + })} + > + Analysis error + + ); + } const diagnostics: string[] = []; if (errorCount > 0) { @@ -54,8 +71,7 @@ const GenerateButton = observer(function GenerateButton({ aria-label={`Select next diagnostic out of ${summary}`} onClick={() => editorStore.nextDiagnostic()} color="error" - startIcon={} - {...(sx === undefined ? {} : { sx })} + startIcon={} > {summary} @@ -67,7 +83,6 @@ const GenerateButton = observer(function GenerateButton({ disabled={!editorStore.opened} color={warningCount > 0 ? 'warning' : 'primary'} startIcon={} - {...(sx === undefined ? {} : { sx })} > {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} @@ -76,7 +91,6 @@ const GenerateButton = observer(function GenerateButton({ GenerateButton.defaultProps = { hideWarnings: false, - sx: undefined, }; export default GenerateButton; diff --git a/subprojects/frontend/src/xtext/SemanticsService.ts b/subprojects/frontend/src/xtext/SemanticsService.ts new file mode 100644 index 00000000..50ec371a --- /dev/null +++ b/subprojects/frontend/src/xtext/SemanticsService.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type EditorStore from '../editor/EditorStore'; + +import type ValidationService from './ValidationService'; +import { SemanticsResult } from './xtextServiceResults'; + +export default class SemanticsService { + constructor( + private readonly store: EditorStore, + private readonly validationService: ValidationService, + ) {} + + onPush(push: unknown): void { + const result = SemanticsResult.parse(push); + this.validationService.setSemanticsIssues(result.issues ?? []); + if (result.error !== undefined) { + this.store.setSemanticsError(result.error); + } else { + this.store.setSemantics(push); + } + this.store.analysisCompleted(); + } +} diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index ee5ebde2..1ac722e1 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts @@ -133,6 +133,7 @@ export default class UpdateService { return; } log.trace('Editor delta', delta); + this.store.analysisStarted(); const result = await this.webSocketClient.send({ resource: this.resourceName, serviceType: 'update', @@ -157,6 +158,7 @@ export default class UpdateService { private async updateFullTextExclusive(): Promise { log.debug('Performing full text update'); this.tracker.prepareFullTextUpdateExclusive(); + this.store.analysisStarted(); const result = await this.webSocketClient.send({ resource: this.resourceName, serviceType: 'update', diff --git a/subprojects/frontend/src/xtext/ValidationService.ts b/subprojects/frontend/src/xtext/ValidationService.ts index 64fb63eb..1a896db3 100644 --- a/subprojects/frontend/src/xtext/ValidationService.ts +++ b/subprojects/frontend/src/xtext/ValidationService.ts @@ -9,7 +9,7 @@ import type { Diagnostic } from '@codemirror/lint'; import type EditorStore from '../editor/EditorStore'; import type UpdateService from './UpdateService'; -import { ValidationResult } from './xtextServiceResults'; +import { Issue, ValidationResult } from './xtextServiceResults'; export default class ValidationService { constructor( @@ -17,11 +17,41 @@ export default class ValidationService { private readonly updateService: UpdateService, ) {} + private lastValidationIssues: Issue[] = []; + + private lastSemanticsIssues: Issue[] = []; + onPush(push: unknown): void { - const { issues } = ValidationResult.parse(push); + ({ issues: this.lastValidationIssues } = ValidationResult.parse(push)); + this.lastSemanticsIssues = []; + this.updateDiagnostics(); + if ( + this.lastValidationIssues.some(({ severity }) => severity === 'error') + ) { + this.store.analysisCompleted(true); + } + } + + onDisconnect(): void { + this.store.updateDiagnostics([]); + this.lastValidationIssues = []; + this.lastSemanticsIssues = []; + } + + setSemanticsIssues(issues: Issue[]): void { + this.lastSemanticsIssues = issues; + this.updateDiagnostics(); + } + + private updateDiagnostics(): void { const allChanges = this.updateService.computeChangesSinceLastUpdate(); const diagnostics: Diagnostic[] = []; - issues.forEach(({ offset, length, severity, description }) => { + function createDiagnostic({ + offset, + length, + severity, + description, + }: Issue): void { if (severity === 'ignore') { return; } @@ -31,11 +61,9 @@ export default class ValidationService { severity, message: description, }); - }); + } + this.lastValidationIssues.forEach(createDiagnostic); + this.lastSemanticsIssues.forEach(createDiagnostic); this.store.updateDiagnostics(diagnostics); } - - onDisconnect(): void { - this.store.updateDiagnostics([]); - } } diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index d145cd30..87778084 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts @@ -17,6 +17,7 @@ import getLogger from '../utils/getLogger'; import ContentAssistService from './ContentAssistService'; import HighlightingService from './HighlightingService'; import OccurrencesService from './OccurrencesService'; +import SemanticsService from './SemanticsService'; import UpdateService from './UpdateService'; import ValidationService from './ValidationService'; import XtextWebSocketClient from './XtextWebSocketClient'; @@ -37,6 +38,8 @@ export default class XtextClient { private readonly occurrencesService: OccurrencesService; + private readonly semanticsService: SemanticsService; + constructor( private readonly store: EditorStore, private readonly pwaStore: PWAStore, @@ -54,6 +57,7 @@ export default class XtextClient { ); this.validationService = new ValidationService(store, this.updateService); this.occurrencesService = new OccurrencesService(store, this.updateService); + this.semanticsService = new SemanticsService(store, this.validationService); } start(): void { @@ -67,6 +71,7 @@ export default class XtextClient { } private onDisconnect(): void { + this.store.analysisCompleted(true); this.highlightingService.onDisconnect(); this.validationService.onDisconnect(); this.occurrencesService.onDisconnect(); @@ -115,7 +120,7 @@ export default class XtextClient { this.validationService.onPush(push); return; case 'semantics': - this.store.setSemantics(push); + this.semanticsService.onPush(push); return; default: throw new Error('Unknown service'); diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index d3b467ad..cae95771 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts @@ -125,3 +125,10 @@ export const FormattingResult = DocumentStateResult.extend({ }); export type FormattingResult = z.infer; + +export const SemanticsResult = z.object({ + error: z.string().optional(), + issues: Issue.array().optional(), +}); + +export type SemanticsResult = z.infer; 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 5f854ac3..5ed65e04 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 @@ -15,12 +15,14 @@ import tools.refinery.language.utils.ProblemDesugarer; import tools.refinery.language.utils.ProblemUtil; import tools.refinery.store.model.ModelStoreBuilder; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.dnf.InvalidClauseException; import tools.refinery.store.query.dnf.Query; import tools.refinery.store.query.dnf.RelationalQuery; import tools.refinery.store.query.literal.*; import tools.refinery.store.query.term.NodeVariable; import tools.refinery.store.query.term.Variable; import tools.refinery.store.reasoning.ReasoningAdapter; +import tools.refinery.store.reasoning.representation.AnyPartialSymbol; import tools.refinery.store.reasoning.representation.PartialRelation; import tools.refinery.store.reasoning.seed.ModelSeed; import tools.refinery.store.reasoning.seed.Seed; @@ -60,7 +62,9 @@ public class ModelInitializer { private final Map relationInfoMap = new LinkedHashMap<>(); - private final Map partialRelationInfoMap = new LinkedHashMap<>(); + private final Map partialRelationInfoMap = new HashMap<>(); + + private Map inverseTrace = new HashMap<>(); private Map relationTrace; @@ -82,6 +86,10 @@ public class ModelInitializer { return relationTrace; } + public Relation getInverseTrace(AnyPartialSymbol partialRelation) { + return inverseTrace.get(partialRelation); + } + public ModelSeed createModel(Problem problem, ModelStoreBuilder storeBuilder) { this.problem = problem; this.storeBuilder = storeBuilder; @@ -172,7 +180,7 @@ public class ModelInitializer { collectPartialRelation(invalidMultiplicityConstraint, 1, TruthValue.FALSE, TruthValue.FALSE); } } else { - throw new IllegalArgumentException("Unknown feature declaration: " + featureDeclaration); + throw new TracedException(featureDeclaration, "Unknown feature declaration"); } } } @@ -189,6 +197,7 @@ public class ModelInitializer { private void putRelationInfo(Relation relation, RelationInfo info) { relationInfoMap.put(relation, info); partialRelationInfoMap.put(info.partialRelation(), info); + inverseTrace.put(info.partialRelation(), relation); } private RelationInfo collectPartialRelation(Relation relation, int arity, TruthValue value, @@ -197,6 +206,7 @@ public class ModelInitializer { var name = getName(relation); var info = new RelationInfo(name, arity, value, defaultValue); partialRelationInfoMap.put(info.partialRelation(), info); + inverseTrace.put(info.partialRelation(), relation); return info; }); } @@ -216,7 +226,11 @@ public class ModelInitializer { } private void collectEnumMetamodel(EnumDeclaration enumDeclaration) { - metamodelBuilder.type(getPartialRelation(enumDeclaration), nodeRelation); + try { + metamodelBuilder.type(getPartialRelation(enumDeclaration), nodeRelation); + } catch (RuntimeException e) { + throw TracedException.addTrace(enumDeclaration, e); + } } private void collectClassDeclarationMetamodel(ClassDeclaration classDeclaration) { @@ -226,13 +240,15 @@ public class ModelInitializer { for (var superType : superTypes) { partialSuperTypes.add(getPartialRelation(superType)); } - metamodelBuilder.type(getPartialRelation(classDeclaration), classDeclaration.isAbstract(), - partialSuperTypes); + try { + metamodelBuilder.type(getPartialRelation(classDeclaration), classDeclaration.isAbstract(), + partialSuperTypes); + } catch (RuntimeException e) { + throw TracedException.addTrace(classDeclaration, e); + } for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) { collectReferenceDeclarationMetamodel(classDeclaration, referenceDeclaration); - } else { - throw new IllegalArgumentException("Unknown feature declaration: " + featureDeclaration); } } } @@ -249,7 +265,11 @@ public class ModelInitializer { oppositeRelation = getPartialRelation(opposite); } var multiplicity = getMultiplicityConstraint(referenceDeclaration); - metamodelBuilder.reference(relation, source, containment, multiplicity, target, oppositeRelation); + try { + metamodelBuilder.reference(relation, source, containment, multiplicity, target, oppositeRelation); + } catch (RuntimeException e) { + throw TracedException.addTrace(classDeclaration, e); + } } private Multiplicity getMultiplicityConstraint(ReferenceDeclaration referenceDeclaration) { @@ -267,7 +287,7 @@ public class ModelInitializer { interval = CardinalityIntervals.between(rangeMultiplicity.getLowerBound(), upperBound < 0 ? UpperCardinalities.UNBOUNDED : UpperCardinalities.atMost(upperBound)); } else { - throw new IllegalArgumentException("Unknown multiplicity: " + problemMultiplicity); + throw new TracedException(problemMultiplicity, "Unknown multiplicity"); } var constraint = getRelationInfo(referenceDeclaration.getInvalidMultiplicity()).partialRelation(); return ConstrainedMultiplicity.of(interval, constraint); @@ -327,13 +347,19 @@ public class ModelInitializer { } private void collectAssertion(Assertion assertion) { - var relation = assertion.getRelation(); var tuple = getTuple(assertion); var value = getTruthValue(assertion.getValue()); + var relation = assertion.getRelation(); + var info = getRelationInfo(relation); + var partialRelation = info.partialRelation(); + if (partialRelation.arity() != tuple.getSize()) { + throw new TracedException(assertion, "Expected %d arguments for %s, got %d instead" + .formatted(partialRelation.arity(), partialRelation, tuple.getSize())); + } if (assertion.isDefault()) { - mergeDefaultValue(relation, tuple, value); + info.defaultAssertions().mergeValue(tuple, value); } else { - mergeValue(relation, tuple, value); + info.assertions().mergeValue(tuple, value); } } @@ -341,10 +367,6 @@ public class ModelInitializer { 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) { @@ -372,7 +394,7 @@ public class ModelInitializer { } else if (argument instanceof WildcardAssertionArgument) { nodes[i] = -1; } else { - throw new IllegalArgumentException("Unknown assertion argument: " + argument); + throw new TracedException(argument, "Unsupported assertion argument"); } } return Tuple.of(nodes); @@ -393,8 +415,24 @@ public class ModelInitializer { private void collectPredicates() { for (var statement : problem.getStatements()) { if (statement instanceof PredicateDefinition predicateDefinition) { - collectPredicateDefinition(predicateDefinition); + collectPredicateDefinitionTraced(predicateDefinition); + } + } + } + + private void collectPredicateDefinitionTraced(PredicateDefinition predicateDefinition) { + try { + collectPredicateDefinition(predicateDefinition); + } catch (InvalidClauseException e) { + int clauseIndex = e.getClauseIndex(); + var bodies = predicateDefinition.getBodies(); + if (clauseIndex < bodies.size()) { + throw new TracedException(bodies.get(clauseIndex), e); + } else { + throw new TracedException(predicateDefinition, e); } + } catch (RuntimeException e) { + throw TracedException.addTrace(predicateDefinition, e); } } @@ -436,13 +474,17 @@ public class ModelInitializer { } var builder = Query.builder(name).parameters(parameters); for (var body : predicateDefinition.getBodies()) { - var localScope = extendScope(parameterMap, body.getImplicitVariables()); - var problemLiterals = body.getLiterals(); - var literals = new ArrayList(commonLiterals); - for (var problemLiteral : problemLiterals) { - toLiterals(problemLiteral, localScope, literals); + try { + var localScope = extendScope(parameterMap, body.getImplicitVariables()); + var problemLiterals = body.getLiterals(); + var literals = new ArrayList<>(commonLiterals); + for (var problemLiteral : problemLiterals) { + toLiteralsTraced(problemLiteral, localScope, literals); + } + builder.clause(literals); + } catch (RuntimeException e) { + throw TracedException.addTrace(body, e); } - builder.clause(literals); } return builder.build(); } @@ -462,13 +504,23 @@ public class ModelInitializer { return localScope; } - private void toLiterals(Expr expr, Map localScope, + private void toLiteralsTraced(Expr expr, Map localScope, + List literals) { + try { + toLiterals(expr, localScope, literals); + } catch (RuntimeException e) { + throw TracedException.addTrace(expr, e); + } + } + + private void toLiterals(Expr expr, Map localScope, List literals) { if (expr instanceof LogicConstant logicConstant) { switch (logicConstant.getLogicValue()) { - case TRUE -> literals.add(BooleanLiteral.TRUE); - case FALSE -> literals.add(BooleanLiteral.FALSE); - default -> throw new IllegalArgumentException("Unsupported literal: " + expr); + case TRUE -> literals.add(BooleanLiteral.TRUE); + case FALSE -> literals.add(BooleanLiteral.FALSE); + default -> throw new TracedException(logicConstant, "Unsupported literal"); } } else if (expr instanceof Atom atom) { var target = getPartialRelation(atom.getRelation()); @@ -478,7 +530,7 @@ public class ModelInitializer { } else if (expr instanceof NegationExpr negationExpr) { var body = negationExpr.getBody(); if (!(body instanceof Atom atom)) { - throw new IllegalArgumentException("Cannot negate literal: " + body); + throw new TracedException(body, "Cannot negate literal"); } var target = getPartialRelation(atom.getRelation()); Constraint constraint; @@ -498,11 +550,12 @@ public class ModelInitializer { boolean positive = switch (comparisonExpr.getOp()) { case EQ -> true; case NOT_EQ -> false; - default -> throw new IllegalArgumentException("Unsupported operator: " + comparisonExpr.getOp()); + default -> throw new TracedException( + comparisonExpr, "Unsupported operator"); }; literals.add(new EquivalenceLiteral(positive, argumentList.get(0), argumentList.get(1))); } else { - throw new IllegalArgumentException("Unsupported literal: " + expr); + throw new TracedException(expr, "Unsupported literal"); } } @@ -512,7 +565,7 @@ public class ModelInitializer { var argumentList = new ArrayList(expressions.size()); for (var expr : expressions) { if (!(expr instanceof VariableOrNodeExpr variableOrNodeExpr)) { - throw new IllegalArgumentException("Unsupported argument: " + expr); + throw new TracedException(expr, "Unsupported argument"); } var variableOrNode = variableOrNodeExpr.getVariableOrNode(); if (variableOrNode instanceof Node node) { @@ -526,10 +579,12 @@ public class ModelInitializer { } else { var variable = localScope.get(problemVariable); if (variable == null) { - throw new IllegalArgumentException("Unknown variable: " + problemVariable.getName()); + throw new TracedException(variableOrNode, "Unknown variable: " + problemVariable.getName()); } argumentList.add(variable); } + } else { + throw new TracedException(variableOrNode, "Unknown argument"); } } return argumentList; diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/TracedException.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/TracedException.java new file mode 100644 index 00000000..38fd8a67 --- /dev/null +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/TracedException.java @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.semantics.model; + +import org.eclipse.emf.ecore.EObject; + +public class TracedException extends RuntimeException { + private final transient EObject sourceElement; + + public TracedException(EObject sourceElement) { + this.sourceElement = sourceElement; + } + + public TracedException(EObject sourceElement, String message) { + super(message); + this.sourceElement = sourceElement; + } + + public TracedException(EObject sourceElement, String message, Throwable cause) { + super(message, cause); + this.sourceElement = sourceElement; + } + + public TracedException(EObject sourceElement, Throwable cause) { + super(cause); + this.sourceElement = sourceElement; + } + + public EObject getSourceElement() { + return sourceElement; + } + + @Override + public String getMessage() { + var message = super.getMessage(); + if (message == null) { + return "Internal error"; + } + return message; + } + + public static TracedException addTrace(EObject sourceElement, Throwable cause) { + if (cause instanceof TracedException tracedException && tracedException.sourceElement != null) { + return tracedException; + } + return new TracedException(sourceElement, cause); + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java new file mode 100644 index 00000000..aa14f39d --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.semantics; + +import tools.refinery.store.map.AnyVersionedMap; +import tools.refinery.store.map.Cursor; +import tools.refinery.store.reasoning.representation.PartialSymbol; +import tools.refinery.store.reasoning.seed.ModelSeed; +import tools.refinery.store.reasoning.seed.Seed; +import tools.refinery.store.tuple.Tuple; +import tools.refinery.viatra.runtime.CancellationToken; + +import java.util.Set; + +class CancellableSeed implements Seed { + private final CancellationToken cancellationToken; + private final Seed seed; + + private CancellableSeed(CancellationToken cancellationToken, Seed seed) { + this.cancellationToken = cancellationToken; + this.seed = seed; + } + + @Override + public int arity() { + return seed.arity(); + } + + @Override + public Class valueType() { + return seed.valueType(); + } + + @Override + public T reducedValue() { + return seed.reducedValue(); + } + + @Override + public T get(Tuple key) { + return seed.get(key); + } + + @Override + public Cursor getCursor(T defaultValue, int nodeCount) { + return new CancellableCursor<>(cancellationToken, seed.getCursor(defaultValue, nodeCount)); + } + + public static ModelSeed wrap(CancellationToken cancellationToken, ModelSeed modelSeed) { + var builder = ModelSeed.builder(modelSeed.getNodeCount()); + for (var partialSymbol : modelSeed.getSeededSymbols()) { + wrap(cancellationToken, (PartialSymbol) partialSymbol, modelSeed, builder); + } + return builder.build(); + } + + private static void wrap(CancellationToken cancellationToken, PartialSymbol partialSymbol, + ModelSeed originalModelSeed, ModelSeed.Builder builder) { + var originalSeed = originalModelSeed.getSeed(partialSymbol); + builder.seed(partialSymbol, new CancellableSeed<>(cancellationToken, originalSeed)); + } + + private record CancellableCursor(CancellationToken cancellationToken, Cursor cursor) + implements Cursor { + @Override + public Tuple getKey() { + return cursor.getKey(); + } + + @Override + public T getValue() { + return cursor.getValue(); + } + + @Override + public boolean isTerminated() { + return cursor.isTerminated(); + } + + @Override + public boolean move() { + cancellationToken.checkCancelled(); + return cursor.move(); + } + + @Override + public boolean isDirty() { + return cursor.isDirty(); + } + + @Override + public Set getDependingMaps() { + return cursor.getDependingMaps(); + } + } +} 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 deleted file mode 100644 index ce34ef6c..00000000 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsErrorResult.java +++ /dev/null @@ -1,9 +0,0 @@ -/* - * 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/SemanticsInternalErrorResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsInternalErrorResult.java new file mode 100644 index 00000000..ff592e93 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsInternalErrorResult.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 SemanticsInternalErrorResult(String error) implements SemanticsResult { +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java new file mode 100644 index 00000000..644bd179 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.semantics; + +import org.eclipse.xtext.web.server.validation.ValidationResult; + +import java.util.List; + +public record SemanticsIssuesResult(List issues) 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 index 92639578..a2e19a2f 100644 --- 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 @@ -7,5 +7,6 @@ package tools.refinery.language.web.semantics; import org.eclipse.xtext.web.server.IServiceResult; -public sealed interface SemanticsResult extends IServiceResult permits SemanticsSuccessResult, SemanticsErrorResult { +public sealed interface SemanticsResult extends IServiceResult permits SemanticsSuccessResult, + SemanticsInternalErrorResult, SemanticsIssuesResult { } 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 39191162..56b2cbc1 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 @@ -5,9 +5,11 @@ */ package tools.refinery.language.web.semantics; +import com.google.gson.JsonObject; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import org.eclipse.xtext.ide.ExecutorServiceProvider; import org.eclipse.xtext.service.OperationCanceledManager; import org.eclipse.xtext.util.CancelIndicator; import org.eclipse.xtext.web.server.model.AbstractCachedService; @@ -19,6 +21,7 @@ import org.slf4j.LoggerFactory; import tools.refinery.language.model.problem.Problem; import tools.refinery.language.web.xtext.server.push.PushWebDocument; +import java.util.List; import java.util.concurrent.*; @Singleton @@ -34,7 +37,12 @@ public class SemanticsService extends AbstractCachedService { @Inject private ValidationService validationService; - private final ExecutorService executorService = Executors.newCachedThreadPool(); + private ExecutorService executorService; + + @Inject + public void setExecutorServiceProvider(ExecutorServiceProvider provider) { + executorService = provider.get(this.getClass().getName()); + } @Override public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) { @@ -42,12 +50,15 @@ public class SemanticsService extends AbstractCachedService { if (LOG.isTraceEnabled()) { start = System.currentTimeMillis(); } - var problem = getProblem(doc, cancelIndicator); - if (problem == null) { + if (hasError(doc, cancelIndicator)) { return null; } + var problem = getProblem(doc); + if (problem == null) { + return new SemanticsSuccessResult(List.of(), new JsonObject()); + } var worker = workerProvider.get(); - worker.setProblem(problem,cancelIndicator); + worker.setProblem(problem, cancelIndicator); var future = executorService.submit(worker); SemanticsResult result = null; try { @@ -58,11 +69,19 @@ public class SemanticsService extends AbstractCachedService { Thread.currentThread().interrupt(); } catch (ExecutionException e) { operationCanceledManager.propagateAsErrorIfCancelException(e.getCause()); - throw new IllegalStateException(e); + LOG.debug("Error while computing semantics", e); + if (e.getCause() instanceof Error error) { + throw error; + } + String message = e.getMessage(); + if (message == null) { + message = "Partial interpretation error"; + } + return new SemanticsInternalErrorResult(message); } catch (TimeoutException e) { future.cancel(true); LOG.trace("Semantics service timeout", e); - return new SemanticsErrorResult("Partial interpretation timed out"); + return new SemanticsInternalErrorResult("Partial interpretation timed out"); } if (LOG.isTraceEnabled()) { long end = System.currentTimeMillis(); @@ -72,17 +91,17 @@ public class SemanticsService extends AbstractCachedService { return result; } - @Nullable - private Problem getProblem(IXtextWebDocument doc, CancelIndicator cancelIndicator) { + private boolean hasError(IXtextWebDocument doc, CancelIndicator cancelIndicator) { if (!(doc instanceof PushWebDocument pushDoc)) { throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc); } var validationResult = pushDoc.getCachedServiceResult(validationService, cancelIndicator, true); - boolean hasError = validationResult.getIssues().stream() + return validationResult.getIssues().stream() .anyMatch(issue -> "error".equals(issue.getSeverity())); - if (hasError) { - return null; - } + } + + @Nullable + private Problem getProblem(IXtextWebDocument doc) { var contents = doc.getResource().getContents(); if (contents.isEmpty()) { return null; 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 25589260..43d0238c 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 @@ -8,13 +8,19 @@ package tools.refinery.language.web.semantics; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.inject.Inject; +import org.eclipse.emf.common.util.Diagnostic; +import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.service.OperationCanceledManager; import org.eclipse.xtext.util.CancelIndicator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.xtext.validation.CheckType; +import org.eclipse.xtext.validation.FeatureBasedDiagnostic; +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.model.ModelInitializer; import tools.refinery.language.semantics.model.SemanticsUtils; +import tools.refinery.language.semantics.model.TracedException; import tools.refinery.store.model.Model; import tools.refinery.store.model.ModelStore; import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; @@ -22,17 +28,19 @@ 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.reasoning.translator.TranslationException; import tools.refinery.store.representation.TruthValue; 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; class SemanticsWorker implements Callable { - private static final Logger LOG = LoggerFactory.getLogger(SemanticsWorker.class); + private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError"; @Inject private SemanticsUtils semanticsUtils; @@ -40,6 +48,9 @@ class SemanticsWorker implements Callable { @Inject private OperationCanceledManager operationCanceledManager; + @Inject + private IDiagnosticConverter diagnosticConverter; + @Inject private ModelInitializer initializer; @@ -71,15 +82,17 @@ class SemanticsWorker implements Callable { cancellationToken.checkCancelled(); var store = builder.build(); cancellationToken.checkCancelled(); - var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); + var cancellableModelSeed = CancellableSeed.wrap(cancellationToken, modelSeed); + var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(cancellableModelSeed); cancellationToken.checkCancelled(); var partialInterpretation = getPartialInterpretation(initializer, model); return new SemanticsSuccessResult(nodeTrace, partialInterpretation); - } catch (RuntimeException e) { - LOG.debug("Error while computing semantics", e); - var message = e.getMessage(); - return new SemanticsErrorResult(message == null ? "Partial interpretation error" : e.getMessage()); + } catch (TracedException e) { + return getTracedErrorResult(e.getSourceElement(), e.getMessage()); + } catch (TranslationException e) { + var sourceElement = initializer.getInverseTrace(e.getPartialSymbol()); + return getTracedErrorResult(sourceElement, e.getMessage()); } } @@ -130,4 +143,19 @@ class SemanticsWorker implements Callable { json.add(value.toString()); return json; } + + private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) { + if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) { + return new SemanticsInternalErrorResult(message); + } + var diagnostic = new FeatureBasedDiagnostic(Diagnostic.ERROR, message, sourceElement, null, 0, + CheckType.EXPENSIVE, DIAGNOSTIC_ID); + var xtextIssues = new ArrayList(); + diagnosticConverter.convertValidatorDiagnostic(diagnostic, xtextIssues::add); + var issues = xtextIssues.stream() + .map(issue -> new ValidationResult.Issue(issue.getMessage(), "error", issue.getLineNumber(), + issue.getColumn(), issue.getOffset(), issue.getLength())) + .toList(); + return new SemanticsIssuesResult(issues); + } } 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 1542c694..2d43fb26 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 @@ -53,10 +53,9 @@ public class PushWebDocument extends XtextWebDocument { public void precomputeServiceResult(AbstractCachedService service, String serviceName, CancelIndicator cancelIndicator, boolean logCacheMiss) { var serviceClass = service.getClass(); - var previousResult = precomputedServices.get(serviceClass); var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); precomputedServices.put(serviceClass, result); - if (result != null && !result.equals(previousResult)) { + if (result != null) { notifyPrecomputationListeners(serviceName, result); } } 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 99ca5420..889a55cb 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(7)); + assertThat(responses, hasSize(8)); assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); assertThat(responses.get(1), startsWith( "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\"," + @@ -108,7 +108,10 @@ class ProblemWebSocketServletIntegrationTest { assertThat(responses.get(5), startsWith( "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\"," + "\"push\":{\"regions\":[")); - assertThat(responses.get(6), startsWith( + assertThat(responses.get(6), equalTo( + "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"validate\"," + + "\"push\":{\"issues\":[]}}")); + assertThat(responses.get(7), startsWith( "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"semantics\"," + "\"push\":{")); } @@ -130,7 +133,7 @@ class ProblemWebSocketServletIntegrationTest { "\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}", Callback.NOOP ); - case 7 -> session.close(); + case 8 -> session.close(); } } } 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 b7142506..22ce1b47 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 @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.junit.jupiter.MockitoExtension; +import tools.refinery.language.web.semantics.SemanticsService; import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider; import tools.refinery.language.web.tests.ProblemWebInjectorProvider; import tools.refinery.language.web.xtext.server.ResponseHandler; @@ -59,11 +60,16 @@ class TransactionExecutorTest { @Inject private AwaitTerminationExecutorServiceProvider executorServices; + @Inject + private SemanticsService semanticsService; + private TransactionExecutor transactionExecutor; @BeforeEach void beforeEach() { transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry); + // Manually re-create the semantics analysis thread pool if it was disposed by the previous test. + semanticsService.setExecutorServiceProvider(executorServices); } @Test @@ -95,7 +101,7 @@ class TransactionExecutorTest { "0"))); var captor = newCaptor(); - verify(responseHandler, times(3)).onResponse(captor.capture()); + verify(responseHandler, times(4)).onResponse(captor.capture()); var newStateId = getStateId("bar", captor.getAllValues().get(0)); assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/InvalidQueryException.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/InvalidQueryException.java new file mode 100644 index 00000000..c39277a0 --- /dev/null +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/InvalidQueryException.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.query; + +public class InvalidQueryException extends RuntimeException { + public InvalidQueryException() { + } + + public InvalidQueryException(String message) { + super(message); + } + + public InvalidQueryException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidQueryException(Throwable cause) { + super(cause); + } +} diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java index 5d77b9aa..8800a155 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java @@ -7,6 +7,7 @@ package tools.refinery.store.query.dnf; import org.jetbrains.annotations.NotNull; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.literal.*; import tools.refinery.store.query.substitution.MapBasedSubstitution; import tools.refinery.store.query.substitution.StatelessSubstitution; @@ -160,7 +161,7 @@ class ClausePostProcessor { // Inputs count as positive, because they are already bound when we evaluate literals. positiveVariables.add(variable); } else if (!existentiallyQuantifiedVariables.contains(variable)) { - throw new IllegalArgumentException("Unbound %s parameter %s" + throw new InvalidQueryException("Unbound %s parameter %s" .formatted(ParameterDirection.OUT, variable)); } } @@ -172,7 +173,7 @@ class ClausePostProcessor { var representative = pair.getKey(); if (!positiveVariables.contains(representative)) { var variableSet = pair.getValue(); - throw new IllegalArgumentException("Variables %s were merged by equivalence but are not bound" + throw new InvalidQueryException("Variables %s were merged by equivalence but are not bound" .formatted(variableSet)); } } @@ -184,7 +185,7 @@ class ClausePostProcessor { for (var variable : literal.getPrivateVariables(positiveVariables)) { var oldLiteral = negativeVariablesMap.put(variable, literal); if (oldLiteral != null) { - throw new IllegalArgumentException("Unbound variable %s appears in multiple literals %s and %s" + throw new InvalidQueryException("Unbound variable %s appears in multiple literals %s and %s" .formatted(variable, oldLiteral, literal)); } } @@ -206,7 +207,7 @@ class ClausePostProcessor { variable.addToSortedLiterals(); } if (!variableToLiteralInputMap.isEmpty()) { - throw new IllegalArgumentException("Unbound input variables %s" + throw new InvalidQueryException("Unbound input variables %s" .formatted(variableToLiteralInputMap.keySet())); } } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java index 55f1aae5..86a1b6b2 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.dnf; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.DnfEqualityChecker; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.SubstitutingLiteralEqualityHelper; @@ -55,7 +56,7 @@ public final class Dnf implements Constraint { FunctionalDependency functionalDependency) { for (var variable : toValidate) { if (!parameterSet.contains(variable)) { - throw new IllegalArgumentException( + throw new InvalidQueryException( "Variable %s of functional dependency %s does not appear in the parameter list %s" .formatted(variable, functionalDependency, symbolicParameters)); } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java index 0538427f..0f9fd366 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java @@ -5,10 +5,8 @@ */ package tools.refinery.store.query.dnf; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.dnf.callback.*; -import tools.refinery.store.query.equality.DnfEqualityChecker; -import tools.refinery.store.query.equality.SubstitutingLiteralEqualityHelper; -import tools.refinery.store.query.equality.SubstitutingLiteralHashCodeHelper; import tools.refinery.store.query.literal.Literal; import tools.refinery.store.query.term.*; @@ -100,7 +98,7 @@ public final class DnfBuilder { public DnfBuilder symbolicParameter(SymbolicParameter symbolicParameter) { var variable = symbolicParameter.getVariable(); if (!parameterVariables.add(variable)) { - throw new IllegalArgumentException("Variable %s is already on the parameter list %s" + throw new InvalidQueryException("Variable %s is already on the parameter list %s" .formatted(variable, parameters)); } parameters.add(symbolicParameter); @@ -218,88 +216,10 @@ public final class DnfBuilder { } public Dnf build() { - var postProcessedClauses = postProcessClauses(); + var postProcessor = new DnfPostProcessor(parameters, clauses); + var postProcessedClauses = postProcessor.postProcessClauses(); return new Dnf(name, Collections.unmodifiableList(parameters), Collections.unmodifiableList(functionalDependencies), Collections.unmodifiableList(postProcessedClauses)); } - - private List postProcessClauses() { - var parameterInfoMap = getParameterInfoMap(); - var postProcessedClauses = new LinkedHashSet(clauses.size()); - for (var literals : clauses) { - var postProcessor = new ClausePostProcessor(parameterInfoMap, literals); - var result = postProcessor.postProcessClause(); - if (result instanceof ClausePostProcessor.ClauseResult clauseResult) { - postProcessedClauses.add(new CanonicalClause(clauseResult.clause())); - } else if (result instanceof ClausePostProcessor.ConstantResult constantResult) { - switch (constantResult) { - case ALWAYS_TRUE -> { - var inputVariables = getInputVariables(); - return List.of(new DnfClause(inputVariables, List.of())); - } - case ALWAYS_FALSE -> { - // Skip this clause because it can never match. - } - default -> throw new IllegalStateException("Unexpected ClausePostProcessor.ConstantResult: " + - constantResult); - } - } else { - throw new IllegalStateException("Unexpected ClausePostProcessor.Result: " + result); - } - } - return postProcessedClauses.stream().map(CanonicalClause::getDnfClause).toList(); - } - - private Map getParameterInfoMap() { - var mutableParameterInfoMap = new LinkedHashMap(); - int arity = parameters.size(); - for (int i = 0; i < arity; i++) { - var parameter = parameters.get(i); - mutableParameterInfoMap.put(parameter.getVariable(), - new ClausePostProcessor.ParameterInfo(parameter.getDirection(), i)); - } - return Collections.unmodifiableMap(mutableParameterInfoMap); - } - - private Set getInputVariables() { - var inputParameters = new LinkedHashSet(); - for (var parameter : parameters) { - if (parameter.getDirection() == ParameterDirection.IN) { - inputParameters.add(parameter.getVariable()); - } - } - return Collections.unmodifiableSet(inputParameters); - } - - private class CanonicalClause { - private final DnfClause dnfClause; - - public CanonicalClause(DnfClause dnfClause) { - this.dnfClause = dnfClause; - } - - public DnfClause getDnfClause() { - return dnfClause; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - var otherCanonicalClause = (CanonicalClause) obj; - var helper = new SubstitutingLiteralEqualityHelper(DnfEqualityChecker.DEFAULT, parameters, parameters); - return dnfClause.equalsWithSubstitution(helper, otherCanonicalClause.dnfClause); - } - - @Override - public int hashCode() { - var helper = new SubstitutingLiteralHashCodeHelper(parameters); - return dnfClause.hashCodeWithSubstitution(helper); - } - } } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfPostProcessor.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfPostProcessor.java new file mode 100644 index 00000000..50236642 --- /dev/null +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfPostProcessor.java @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.query.dnf; + +import tools.refinery.store.query.InvalidQueryException; +import tools.refinery.store.query.equality.DnfEqualityChecker; +import tools.refinery.store.query.equality.SubstitutingLiteralEqualityHelper; +import tools.refinery.store.query.equality.SubstitutingLiteralHashCodeHelper; +import tools.refinery.store.query.literal.Literal; +import tools.refinery.store.query.term.ParameterDirection; +import tools.refinery.store.query.term.Variable; + +import java.util.*; + +class DnfPostProcessor { + private final List parameters; + private final List> clauses; + + public DnfPostProcessor(List parameters, List> clauses) { + this.parameters = parameters; + this.clauses = clauses; + } + + public List postProcessClauses() { + var parameterInfoMap = getParameterInfoMap(); + var postProcessedClauses = new LinkedHashSet(clauses.size()); + int index = 0; + for (var literals : clauses) { + var postProcessor = new ClausePostProcessor(parameterInfoMap, literals); + ClausePostProcessor.Result result; + try { + result = postProcessor.postProcessClause(); + } catch (InvalidQueryException e) { + throw new InvalidClauseException(index, e); + } + if (result instanceof ClausePostProcessor.ClauseResult clauseResult) { + postProcessedClauses.add(new CanonicalClause(clauseResult.clause())); + } else if (result instanceof ClausePostProcessor.ConstantResult constantResult) { + switch (constantResult) { + case ALWAYS_TRUE -> { + var inputVariables = getInputVariables(); + return List.of(new DnfClause(inputVariables, List.of())); + } + case ALWAYS_FALSE -> { + // Skip this clause because it can never match. + } + default -> throw new IllegalStateException("Unexpected ClausePostProcessor.ConstantResult: " + + constantResult); + } + } else { + throw new IllegalStateException("Unexpected ClausePostProcessor.Result: " + result); + } + index++; + } + return postProcessedClauses.stream().map(CanonicalClause::getDnfClause).toList(); + } + + private Map getParameterInfoMap() { + var mutableParameterInfoMap = new LinkedHashMap(); + int arity = parameters.size(); + for (int i = 0; i < arity; i++) { + var parameter = parameters.get(i); + mutableParameterInfoMap.put(parameter.getVariable(), + new ClausePostProcessor.ParameterInfo(parameter.getDirection(), i)); + } + return Collections.unmodifiableMap(mutableParameterInfoMap); + } + + private Set getInputVariables() { + var inputParameters = new LinkedHashSet(); + for (var parameter : parameters) { + if (parameter.getDirection() == ParameterDirection.IN) { + inputParameters.add(parameter.getVariable()); + } + } + return Collections.unmodifiableSet(inputParameters); + } + + private class CanonicalClause { + private final DnfClause dnfClause; + + public CanonicalClause(DnfClause dnfClause) { + this.dnfClause = dnfClause; + } + + public DnfClause getDnfClause() { + return dnfClause; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + var otherCanonicalClause = (CanonicalClause) obj; + var helper = new SubstitutingLiteralEqualityHelper(DnfEqualityChecker.DEFAULT, parameters, parameters); + return dnfClause.equalsWithSubstitution(helper, otherCanonicalClause.dnfClause); + } + + @Override + public int hashCode() { + var helper = new SubstitutingLiteralHashCodeHelper(parameters); + return dnfClause.hashCodeWithSubstitution(helper); + } + } +} diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java index b00b2cb7..aef07ee3 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java @@ -5,6 +5,8 @@ */ package tools.refinery.store.query.dnf; +import tools.refinery.store.query.InvalidQueryException; + import java.util.HashSet; import java.util.Set; @@ -13,7 +15,7 @@ public record FunctionalDependency(Set forEach, Set unique) { var uniqueForEach = new HashSet<>(unique); uniqueForEach.retainAll(forEach); if (!uniqueForEach.isEmpty()) { - throw new IllegalArgumentException("Variables %s appear on both sides of the functional dependency" + throw new InvalidQueryException("Variables %s appear on both sides of the functional dependency" .formatted(uniqueForEach)); } } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java index bf7651ad..225f6844 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.dnf; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.literal.CallPolarity; import tools.refinery.store.query.term.Aggregator; import tools.refinery.store.query.term.AssignedValue; @@ -26,14 +27,14 @@ public final class FunctionalQuery extends Query { var parameter = parameters.get(i); var parameterType = parameter.tryGetType(); if (parameterType.isPresent()) { - throw new IllegalArgumentException("Expected parameter %s of %s to be a node variable, got %s instead" + throw new InvalidQueryException("Expected parameter %s of %s to be a node variable, got %s instead" .formatted(parameter, dnf, parameterType.get().getName())); } } var outputParameter = parameters.get(outputIndex); var outputParameterType = outputParameter.tryGetType(); if (outputParameterType.isEmpty() || !outputParameterType.get().equals(type)) { - throw new IllegalArgumentException("Expected parameter %s of %s to be %s, but got %s instead".formatted( + throw new InvalidQueryException("Expected parameter %s of %s to be %s, but got %s instead".formatted( outputParameter, dnf, type, outputParameterType.map(Class::getName).orElse("node"))); } this.type = type; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/InvalidClauseException.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/InvalidClauseException.java new file mode 100644 index 00000000..747574b9 --- /dev/null +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/InvalidClauseException.java @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.query.dnf; + +import tools.refinery.store.query.InvalidQueryException; + +public class InvalidClauseException extends InvalidQueryException { + private final int clauseIndex; + + public InvalidClauseException(int clauseIndex) { + this.clauseIndex = clauseIndex; + } + + public InvalidClauseException(int clauseIndex, String message) { + super(message); + this.clauseIndex = clauseIndex; + } + + public InvalidClauseException(int clauseIndex, String message, Throwable cause) { + super(message, cause); + this.clauseIndex = clauseIndex; + } + + public InvalidClauseException(int clauseIndex, Throwable cause) { + super(cause); + this.clauseIndex = clauseIndex; + } + + public int getClauseIndex() { + return clauseIndex; + } +} diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java index 618fb595..98f71e11 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.dnf; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.literal.CallLiteral; import tools.refinery.store.query.literal.CallPolarity; import tools.refinery.store.query.term.AssignedValue; @@ -19,7 +20,7 @@ public final class RelationalQuery extends Query { for (var parameter : dnf.getSymbolicParameters()) { var parameterType = parameter.tryGetType(); if (parameterType.isPresent()) { - throw new IllegalArgumentException("Expected parameter %s of %s to be a node variable, got %s instead" + throw new InvalidQueryException("Expected parameter %s of %s to be a node variable, got %s instead" .formatted(parameter, dnf, parameterType.get().getName())); } } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java index 3722f7f9..0e99d441 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.literal; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -27,7 +28,7 @@ public abstract class AbstractCallLiteral extends AbstractLiteral { protected AbstractCallLiteral(Constraint target, List arguments) { int arity = target.arity(); if (arguments.size() != arity) { - throw new IllegalArgumentException("%s needs %d arguments, but got %s".formatted(target.name(), + throw new InvalidQueryException("%s needs %d arguments, but got %s".formatted(target.name(), target.arity(), arguments.size())); } this.target = target; @@ -39,7 +40,7 @@ public abstract class AbstractCallLiteral extends AbstractLiteral { var argument = arguments.get(i); var parameter = parameters.get(i); if (!parameter.isAssignable(argument)) { - throw new IllegalArgumentException("Argument %d of %s is not assignable to parameter %s" + throw new InvalidQueryException("Argument %d of %s is not assignable to parameter %s" .formatted(i, target, parameter)); } switch (parameter.getDirection()) { diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCountLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCountLiteral.java index 75f4bd49..9bb572c0 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCountLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCountLiteral.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.literal; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.term.ConstantTerm; @@ -26,11 +27,11 @@ public abstract class AbstractCountLiteral extends AbstractCallLiteral { List arguments) { super(target, arguments); if (!resultVariable.getType().equals(resultType)) { - throw new IllegalArgumentException("Count result variable %s must be of type %s, got %s instead".formatted( + throw new InvalidQueryException("Count result variable %s must be of type %s, got %s instead".formatted( resultVariable, resultType, resultVariable.getType().getName())); } if (arguments.contains(resultVariable)) { - throw new IllegalArgumentException("Count result variable %s must not appear in the argument list" + throw new InvalidQueryException("Count result variable %s must not appear in the argument list" .formatted(resultVariable)); } this.resultType = resultType; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java index a2f8e009..e3acfacc 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.literal; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -26,19 +27,19 @@ public class AggregationLiteral extends AbstractCallLiteral { DataVariable inputVariable, Constraint target, List arguments) { super(target, arguments); if (!inputVariable.getType().equals(aggregator.getInputType())) { - throw new IllegalArgumentException("Input variable %s must of type %s, got %s instead".formatted( + throw new InvalidQueryException("Input variable %s must of type %s, got %s instead".formatted( inputVariable, aggregator.getInputType().getName(), inputVariable.getType().getName())); } if (!getArgumentsOfDirection(ParameterDirection.OUT).contains(inputVariable)) { - throw new IllegalArgumentException("Input variable %s must be bound with direction %s in the argument list" + throw new InvalidQueryException("Input variable %s must be bound with direction %s in the argument list" .formatted(inputVariable, ParameterDirection.OUT)); } if (!resultVariable.getType().equals(aggregator.getResultType())) { - throw new IllegalArgumentException("Result variable %s must of type %s, got %s instead".formatted( + throw new InvalidQueryException("Result variable %s must of type %s, got %s instead".formatted( resultVariable, aggregator.getResultType().getName(), resultVariable.getType().getName())); } if (arguments.contains(resultVariable)) { - throw new IllegalArgumentException("Result variable %s must not appear in the argument list".formatted( + throw new InvalidQueryException("Result variable %s must not appear in the argument list".formatted( resultVariable)); } this.resultVariable = resultVariable; @@ -66,7 +67,7 @@ public class AggregationLiteral extends AbstractCallLiteral { @Override public Set getInputVariables(Set positiveVariablesInClause) { if (positiveVariablesInClause.contains(inputVariable)) { - throw new IllegalArgumentException("Aggregation variable %s must not be bound".formatted(inputVariable)); + throw new InvalidQueryException("Aggregation variable %s must not be bound".formatted(inputVariable)); } return super.getInputVariables(positiveVariablesInClause); } @@ -80,7 +81,7 @@ public class AggregationLiteral extends AbstractCallLiteral { yield emptyValue == null ? BooleanLiteral.FALSE : resultVariable.assign(new ConstantTerm<>(resultVariable.getType(), emptyValue)); } - case ALWAYS_TRUE -> throw new IllegalArgumentException("Trying to aggregate over an infinite set"); + case ALWAYS_TRUE -> throw new InvalidQueryException("Trying to aggregate over an infinite set"); case NOT_REDUCIBLE -> this; }; } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java index d8a4b494..dadf487f 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.literal; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -16,18 +17,20 @@ import java.util.Collections; import java.util.Objects; import java.util.Set; +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") public class AssignLiteral extends AbstractLiteral { private final DataVariable variable; private final Term term; public AssignLiteral(DataVariable variable, Term term) { if (!term.getType().equals(variable.getType())) { - throw new IllegalArgumentException("Term %s must be of type %s, got %s instead".formatted( + throw new InvalidQueryException("Term %s must be of type %s, got %s instead".formatted( term, variable.getType().getName(), term.getType().getName())); } var inputVariables = term.getInputVariables(); if (inputVariables.contains(variable)) { - throw new IllegalArgumentException("Result variable %s must not appear in the term %s".formatted( + throw new InvalidQueryException("Result variable %s must not appear in the term %s".formatted( variable, term)); } this.variable = variable; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java index 1b05943d..2d0e4e97 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.literal; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -25,14 +26,14 @@ public final class CallLiteral extends AbstractCallLiteral implements CanNegate< int arity = target.arity(); if (polarity.isTransitive()) { if (arity != 2) { - throw new IllegalArgumentException("Transitive closures can only take binary relations"); + throw new InvalidQueryException("Transitive closures can only take binary relations"); } if (parameters.get(0).isDataVariable() || parameters.get(1).isDataVariable()) { - throw new IllegalArgumentException("Transitive closures can only be computed over nodes"); + throw new InvalidQueryException("Transitive closures can only be computed over nodes"); } if (parameters.get(0).getDirection() != ParameterDirection.OUT || parameters.get(1).getDirection() != ParameterDirection.OUT) { - throw new IllegalArgumentException("Transitive closures cannot take input parameters"); + throw new InvalidQueryException("Transitive closures cannot take input parameters"); } } this.polarity = polarity; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java index ca70b0fd..716c7109 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java @@ -5,6 +5,8 @@ */ package tools.refinery.store.query.literal; +import tools.refinery.store.query.InvalidQueryException; + public enum CallPolarity { POSITIVE(true, false), NEGATIVE(false, false), @@ -31,7 +33,7 @@ public enum CallPolarity { return switch (this) { case POSITIVE -> NEGATIVE; case NEGATIVE -> POSITIVE; - case TRANSITIVE -> throw new IllegalArgumentException("Transitive polarity cannot be negated"); + case TRANSITIVE -> throw new InvalidQueryException("Transitive polarity cannot be negated"); }; } } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CheckLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CheckLiteral.java index 1271183a..dfedd2cb 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CheckLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CheckLiteral.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.literal; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -18,12 +19,14 @@ import java.util.Collections; import java.util.Objects; import java.util.Set; +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") public class CheckLiteral extends AbstractLiteral implements CanNegate { private final Term term; public CheckLiteral(Term term) { if (!term.getType().equals(Boolean.class)) { - throw new IllegalArgumentException("Term %s must be of type %s, got %s instead".formatted( + throw new InvalidQueryException("Term %s must be of type %s, got %s instead".formatted( term, Boolean.class.getName(), term.getType().getName())); } this.term = term; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java index 9a0c22d1..7343f709 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.literal; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -22,7 +23,7 @@ public final class EquivalenceLiteral extends AbstractLiteral implements CanNega public EquivalenceLiteral(boolean positive, Variable left, Variable right) { if (!left.tryGetType().equals(right.tryGetType())) { - throw new IllegalArgumentException("Variables %s and %s of different type cannot be equivalent" + throw new InvalidQueryException("Variables %s and %s of different type cannot be equivalent" .formatted(left, right)); } this.positive = positive; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java index f6545f9f..f7323947 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.literal; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -32,14 +33,14 @@ public class RepresentativeElectionLiteral extends AbstractCallLiteral { var parameters = target.getParameters(); int arity = target.arity(); if (arity != 2) { - throw new IllegalArgumentException("SCCs can only take binary relations"); + throw new InvalidQueryException("SCCs can only take binary relations"); } if (parameters.get(0).isDataVariable() || parameters.get(1).isDataVariable()) { - throw new IllegalArgumentException("SCCs can only be computed over nodes"); + throw new InvalidQueryException("SCCs can only be computed over nodes"); } if (parameters.get(0).getDirection() != ParameterDirection.OUT || parameters.get(1).getDirection() != ParameterDirection.OUT) { - throw new IllegalArgumentException("SCCs cannot take input parameters"); + throw new InvalidQueryException("SCCs cannot take input parameters"); } } @@ -72,7 +73,7 @@ public class RepresentativeElectionLiteral extends AbstractCallLiteral { var reduction = getTarget().getReduction(); return switch (reduction) { case ALWAYS_FALSE -> BooleanLiteral.FALSE; - case ALWAYS_TRUE -> throw new IllegalArgumentException( + case ALWAYS_TRUE -> throw new InvalidQueryException( "Trying to elect representatives over an infinite set"); case NOT_REDUCIBLE -> this; }; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java index 4d88051b..3801bc11 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.term; import org.jetbrains.annotations.Nullable; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import java.util.Optional; @@ -33,7 +34,7 @@ public abstract sealed class AnyDataVariable extends Variable implements AnyTerm @Override public NodeVariable asNodeVariable() { - throw new IllegalStateException("%s is a data variable".formatted(this)); + throw new InvalidQueryException("%s is a data variable".formatted(this)); } @Override diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java index 09c86db6..cdbf592a 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.term; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -26,11 +27,11 @@ public abstract class BinaryTerm extends AbstractTerm { protected BinaryTerm(Class type, Class leftType, Class rightType, Term left, Term right) { super(type); if (!left.getType().equals(leftType)) { - throw new IllegalArgumentException("Expected left %s to be of type %s, got %s instead".formatted( + throw new InvalidQueryException("Expected left %s to be of type %s, got %s instead".formatted( left, leftType.getName(), left.getType().getName())); } if (!right.getType().equals(rightType)) { - throw new IllegalArgumentException("Expected right %s to be of type %s, got %s instead".formatted( + throw new InvalidQueryException("Expected right %s to be of type %s, got %s instead".formatted( right, rightType.getName(), right.getType().getName())); } this.leftType = leftType; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java index e722c84f..415ae286 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.term; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -21,7 +22,7 @@ public final class ConstantTerm extends AbstractTerm { public ConstantTerm(Class type, T value) { super(type); if (value != null && !type.isInstance(value)) { - throw new IllegalArgumentException("Value %s is not an instance of %s".formatted(value, type.getName())); + throw new InvalidQueryException("Value %s is not an instance of %s".formatted(value, type.getName())); } this.value = value; } diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java index 9b62e545..2206b522 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.term; import org.jetbrains.annotations.Nullable; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.literal.EquivalenceLiteral; @@ -41,8 +42,8 @@ public final class DataVariable extends AnyDataVariable implements Term { @Override public DataVariable asDataVariable(Class newType) { if (!getType().equals(newType)) { - throw new IllegalStateException("%s is not of type %s but of type %s".formatted(this, newType.getName(), - getType().getName())); + throw new InvalidQueryException("%s is not of type %s but of type %s" + .formatted(this, newType.getName(), getType().getName())); } @SuppressWarnings("unchecked") var result = (DataVariable) this; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java index 2f9c8bf1..53c32e20 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.term; import org.jetbrains.annotations.Nullable; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.literal.ConstantLiteral; import tools.refinery.store.query.literal.EquivalenceLiteral; @@ -48,7 +49,7 @@ public final class NodeVariable extends Variable { @Override public DataVariable asDataVariable(Class type) { - throw new IllegalStateException("%s is a node variable".formatted(this)); + throw new InvalidQueryException("%s is a node variable".formatted(this)); } @Override diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java index cd0739be..da83f3c3 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java @@ -6,8 +6,8 @@ package tools.refinery.store.query.term; public enum ParameterDirection { - OUT("@Out"), - IN("@In"); + OUT("out"), + IN("in"); private final String name; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java index 6451ea00..a464ece5 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.term; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.equality.LiteralHashCodeHelper; import tools.refinery.store.query.substitution.Substitution; @@ -22,7 +23,7 @@ public abstract class UnaryTerm extends AbstractTerm { protected UnaryTerm(Class type, Class bodyType, Term body) { super(type); if (!body.getType().equals(bodyType)) { - throw new IllegalArgumentException("Expected body %s to be of type %s, got %s instead".formatted(body, + throw new InvalidQueryException("Expected body %s to be of type %s, got %s instead".formatted(body, bodyType.getName(), body.getType().getName())); } this.bodyType = bodyType; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java index abae6e5c..924277ed 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.query.view; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.tuple.Tuple; import tools.refinery.store.representation.Symbol; @@ -66,7 +67,7 @@ public class FilteredView extends TuplePreservingView { // The predicate doesn't need to handle the default value if it is null. } if (matchesDefaultValue) { - throw new IllegalArgumentException("Tuples with default value %s cannot be enumerated in %s" + throw new InvalidQueryException("Tuples with default value %s cannot be enumerated in %s" .formatted(defaultValue, getSymbol())); } } diff --git a/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/DnfToDefinitionStringTest.java b/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/DnfToDefinitionStringTest.java index d75d7f17..12cfaa4e 100644 --- a/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/DnfToDefinitionStringTest.java +++ b/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/DnfToDefinitionStringTest.java @@ -50,7 +50,7 @@ class DnfToDefinitionStringTest { var dnf = Dnf.builder("Example").parameter(p, ParameterDirection.IN).clause().build(); assertThat(dnf.toDefinitionString(), is(""" - pred Example(@In p) <-> + pred Example(in p) <-> . """)); } @@ -73,7 +73,7 @@ class DnfToDefinitionStringTest { .build(); assertThat(dnf.toDefinitionString(), is(""" - pred Example(@In p) <-> + pred Example(in p) <-> !(@RelationView("key") friend(p, q)). """)); } diff --git a/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/TopologicalSortTest.java b/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/TopologicalSortTest.java index e22dbb21..854bd469 100644 --- a/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/TopologicalSortTest.java +++ b/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/TopologicalSortTest.java @@ -6,6 +6,7 @@ package tools.refinery.store.query.dnf; import org.junit.jupiter.api.Test; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.term.NodeVariable; import tools.refinery.store.query.term.ParameterDirection; import tools.refinery.store.query.term.Variable; @@ -80,7 +81,7 @@ class TopologicalSortTest { example.call(r, t, q, s), friendView.call(r, t) ); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidQueryException.class, builder::build); } @Test @@ -93,7 +94,7 @@ class TopologicalSortTest { example.call(p, q, r, s), example.call(r, t, q, s) ); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidQueryException.class, builder::build); } @Test @@ -107,6 +108,6 @@ class TopologicalSortTest { example.call(r, t, q, s), example.call(p, q, r, t) ); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidQueryException.class, builder::build); } } diff --git a/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/VariableDirectionTest.java b/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/VariableDirectionTest.java index bfeaa447..fc3f5d48 100644 --- a/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/VariableDirectionTest.java +++ b/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/VariableDirectionTest.java @@ -48,7 +48,7 @@ class VariableDirectionTest { @MethodSource("clausesWithVariableInput") void unboundOutVariableTest(List clause) { var builder = Dnf.builder().parameter(p, ParameterDirection.OUT).clause(clause); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidClauseException.class, builder::build); } @ParameterizedTest @@ -100,7 +100,7 @@ class VariableDirectionTest { var clauseWithEquivalence = new ArrayList(clause); clauseWithEquivalence.add(r.isEquivalent(p)); var builder = Dnf.builder().clause(clauseWithEquivalence); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidClauseException.class, builder::build); } static Stream clausesNotBindingVariable() { @@ -118,7 +118,7 @@ class VariableDirectionTest { @MethodSource("literalsWithPrivateVariable") void unboundTwicePrivateVariableTest(Literal literal) { var builder = Dnf.builder().clause(not(personView.call(p)), literal); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidClauseException.class, builder::build); } @ParameterizedTest @@ -126,7 +126,7 @@ class VariableDirectionTest { void unboundTwiceByEquivalencePrivateVariableTest(Literal literal) { var r = Variable.of("r"); var builder = Dnf.builder().clause(not(personView.call(r)), r.isEquivalent(p), literal); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidClauseException.class, builder::build); } static Stream literalsWithPrivateVariable() { @@ -159,7 +159,7 @@ class VariableDirectionTest { @MethodSource("literalsWithRequiredVariableInput") void unboundPrivateVariableTest(Literal literal) { var builder = Dnf.builder().clause(literal); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidClauseException.class, builder::build); } @ParameterizedTest diff --git a/subprojects/store-query/src/test/java/tools/refinery/store/query/literal/AggregationLiteralTest.java b/subprojects/store-query/src/test/java/tools/refinery/store/query/literal/AggregationLiteralTest.java index 35910e08..ddd57e96 100644 --- a/subprojects/store-query/src/test/java/tools/refinery/store/query/literal/AggregationLiteralTest.java +++ b/subprojects/store-query/src/test/java/tools/refinery/store/query/literal/AggregationLiteralTest.java @@ -7,15 +7,16 @@ package tools.refinery.store.query.literal; import org.junit.jupiter.api.Test; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.dnf.Dnf; +import tools.refinery.store.query.dnf.InvalidClauseException; import tools.refinery.store.query.term.*; import java.util.List; import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; import static tools.refinery.store.query.literal.Literals.not; @@ -57,13 +58,13 @@ class AggregationLiteralTest { @Test void missingAggregationVariableTest() { var aggregation = fakeConstraint.aggregateBy(y, INT_SUM, p, z); - assertThrows(IllegalArgumentException.class, () -> x.assign(aggregation)); + assertThrows(InvalidQueryException.class, () -> x.assign(aggregation)); } @Test void circularAggregationVariableTest() { var aggregation = fakeConstraint.aggregateBy(x, INT_SUM, p, x); - assertThrows(IllegalArgumentException.class, () -> x.assign(aggregation)); + assertThrows(InvalidQueryException.class, () -> x.assign(aggregation)); } @Test @@ -73,7 +74,7 @@ class AggregationLiteralTest { not(fakeConstraint.call(p, y)), x.assign(fakeConstraint.aggregateBy(y, INT_SUM, p, y)) ); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidClauseException.class, builder::build); } @Test @@ -83,6 +84,6 @@ class AggregationLiteralTest { y.assign(constant(27)), x.assign(fakeConstraint.aggregateBy(y, INT_SUM, p, y)) ); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(InvalidClauseException.class, builder::build); } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/literal/ModalConstraint.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/literal/ModalConstraint.java index 6e0e91e1..2235a95d 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/literal/ModalConstraint.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/literal/ModalConstraint.java @@ -6,6 +6,7 @@ package tools.refinery.store.reasoning.literal; import tools.refinery.store.query.Constraint; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.equality.LiteralEqualityHelper; import tools.refinery.store.query.literal.Reduction; import tools.refinery.store.query.term.Parameter; @@ -17,7 +18,7 @@ public record ModalConstraint(Modality modality, Concreteness concreteness, Cons implements Constraint { public ModalConstraint { if (constraint instanceof AnySymbolView || constraint instanceof ModalConstraint) { - throw new IllegalArgumentException("Already concrete constraints cannot be abstracted"); + throw new InvalidQueryException("Already concrete constraints cannot be abstracted"); } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/literal/PartialLiterals.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/literal/PartialLiterals.java index 2c879397..2614c26e 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/literal/PartialLiterals.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/literal/PartialLiterals.java @@ -5,6 +5,7 @@ */ package tools.refinery.store.reasoning.literal; +import tools.refinery.store.query.InvalidQueryException; import tools.refinery.store.query.literal.CallLiteral; public final class PartialLiterals { @@ -31,7 +32,7 @@ public final class PartialLiterals { public static CallLiteral addModality(CallLiteral literal, Modality modality, Concreteness concreteness) { var target = literal.getTarget(); if (target instanceof ModalConstraint) { - throw new IllegalArgumentException("Literal %s already has modality".formatted(literal)); + throw new InvalidQueryException("Literal %s already has modality".formatted(literal)); } var polarity = literal.getPolarity(); var modalTarget = new ModalConstraint(modality.commute(polarity), concreteness, target); diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/seed/ModelSeed.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/seed/ModelSeed.java index 28e6258e..e6b3eaf9 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/seed/ModelSeed.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/seed/ModelSeed.java @@ -13,6 +13,7 @@ import tools.refinery.store.tuple.Tuple; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; public class ModelSeed { @@ -43,6 +44,10 @@ public class ModelSeed { return seeds.containsKey(symbol); } + public Set getSeededSymbols() { + return Collections.unmodifiableSet(seeds.keySet()); + } + public Cursor getCursor(PartialSymbol partialSymbol, A defaultValue) { return getSeed(partialSymbol).getCursor(defaultValue, nodeCount); } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/TranslationException.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/TranslationException.java new file mode 100644 index 00000000..edb886ba --- /dev/null +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/TranslationException.java @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.reasoning.translator; + +import tools.refinery.store.reasoning.representation.AnyPartialSymbol; + +public class TranslationException extends RuntimeException { + private final transient AnyPartialSymbol partialSymbol; + + public TranslationException(AnyPartialSymbol partialSymbol) { + this.partialSymbol = partialSymbol; + } + + public TranslationException(AnyPartialSymbol partialSymbol, String message) { + super(message); + this.partialSymbol = partialSymbol; + } + + public TranslationException(AnyPartialSymbol partialSymbol, String message, Throwable cause) { + super(message, cause); + this.partialSymbol = partialSymbol; + } + + public TranslationException(AnyPartialSymbol partialSymbol, Throwable cause) { + super(cause); + this.partialSymbol = partialSymbol; + } + + public AnyPartialSymbol getPartialSymbol() { + return partialSymbol; + } +} diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentInfo.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentInfo.java index 1087e54d..e3457fa7 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentInfo.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentInfo.java @@ -6,17 +6,18 @@ package tools.refinery.store.reasoning.translator.containment; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.reasoning.translator.multiplicity.Multiplicity; public record ContainmentInfo(PartialRelation sourceType, Multiplicity multiplicity, PartialRelation targetType) { public ContainmentInfo { if (sourceType.arity() != 1) { - throw new IllegalArgumentException("Expected source type %s to be of arity 1, got %d instead" - .formatted(sourceType, sourceType.arity())); + throw new TranslationException(sourceType, "Expected source type %s to be of arity 1, got %d instead" + .formatted(sourceType, sourceType.arity())); } if (targetType.arity() != 1) { - throw new IllegalArgumentException("Expected target type %s to be of arity 1, got %d instead" + throw new TranslationException(targetType, "Expected target type %s to be of arity 1, got %d instead" .formatted(targetType, targetType.arity())); } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/ContainedTypeHierarchyBuilder.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/ContainedTypeHierarchyBuilder.java index cc43bce6..a21da3d4 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/ContainedTypeHierarchyBuilder.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/ContainedTypeHierarchyBuilder.java @@ -6,6 +6,7 @@ package tools.refinery.store.reasoning.translator.metamodel; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.reasoning.translator.containment.ContainmentHierarchyTranslator; import tools.refinery.store.reasoning.translator.typehierarchy.TypeHierarchyBuilder; @@ -23,7 +24,7 @@ public class ContainedTypeHierarchyBuilder extends TypeHierarchyBuilder { for (var containedType : containedTypes) { var currentInfo = typeInfoMap.get(containedType); if (currentInfo == null) { - throw new IllegalArgumentException("Invalid contained type: " + containedType); + throw new TranslationException(containedType, "Invalid contained type: " + containedType); } var newInfo = currentInfo.addSupertype(ContainmentHierarchyTranslator.CONTAINED_SYMBOL); typeInfoMap.put(containedType, newInfo); diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java index d0732edc..ad0288ed 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java @@ -6,6 +6,7 @@ package tools.refinery.store.reasoning.translator.metamodel; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.reasoning.translator.containment.ContainmentHierarchyTranslator; import tools.refinery.store.reasoning.translator.containment.ContainmentInfo; import tools.refinery.store.reasoning.translator.crossreference.DirectedCrossReferenceInfo; @@ -13,7 +14,6 @@ import tools.refinery.store.reasoning.translator.crossreference.UndirectedCrossR import tools.refinery.store.reasoning.translator.multiplicity.Multiplicity; import tools.refinery.store.reasoning.translator.multiplicity.UnconstrainedMultiplicity; import tools.refinery.store.reasoning.translator.typehierarchy.TypeInfo; -import tools.refinery.store.representation.cardinality.CardinalityIntervals; import java.util.*; @@ -70,12 +70,13 @@ public class MetamodelBuilder { public MetamodelBuilder reference(PartialRelation linkType, ReferenceInfo info) { if (linkType.arity() != 2) { - throw new IllegalArgumentException("Only references of arity 2 are supported, got %s with %d instead" - .formatted(linkType, linkType.arity())); + throw new TranslationException(linkType, + "Only references of arity 2 are supported, got %s with %d instead".formatted( + linkType, linkType.arity())); } var putResult = referenceInfoMap.put(linkType, info); if (putResult != null && !putResult.equals(info)) { - throw new IllegalArgumentException("Duplicate reference info for partial relation: " + linkType); + throw new TranslationException(linkType, "Duplicate reference info for partial relation: " + linkType); } return this; } @@ -154,11 +155,11 @@ public class MetamodelBuilder { var sourceType = info.sourceType(); var targetType = info.targetType(); if (typeHierarchyBuilder.isInvalidType(sourceType)) { - throw new IllegalArgumentException("Source type %s of %s is not in type hierarchy" + throw new TranslationException(linkType, "Source type %s of %s is not in type hierarchy" .formatted(sourceType, linkType)); } if (typeHierarchyBuilder.isInvalidType(targetType)) { - throw new IllegalArgumentException("Target type %s of %s is not in type hierarchy" + throw new TranslationException(linkType, "Target type %s of %s is not in type hierarchy" .formatted(targetType, linkType)); } var opposite = info.opposite(); @@ -173,8 +174,9 @@ public class MetamodelBuilder { } if (opposite.equals(linkType)) { if (!sourceType.equals(targetType)) { - throw new IllegalArgumentException("Target %s of undirected reference %s differs from source %s" - .formatted(targetType, linkType, sourceType)); + throw new TranslationException(linkType, + "Target %s of undirected reference %s differs from source %s".formatted( + targetType, linkType, sourceType)); } undirectedCrossReferences.put(linkType, new UndirectedCrossReferenceInfo(sourceType, info.multiplicity())); @@ -183,8 +185,8 @@ public class MetamodelBuilder { oppositeReferences.put(opposite, linkType); } if (info.containment()) { - if (targetMultiplicity.multiplicity().meet(CardinalityIntervals.ONE).isEmpty()) { - throw new IllegalArgumentException("Invalid opposite %s with multiplicity %s of containment %s" + if (!UnconstrainedMultiplicity.INSTANCE.equals(targetMultiplicity)) { + throw new TranslationException(opposite, "Invalid opposite %s with multiplicity %s of containment %s" .formatted(opposite, targetMultiplicity, linkType)); } containedTypes.add(targetType); @@ -200,23 +202,23 @@ public class MetamodelBuilder { var sourceType = info.sourceType(); var targetType = info.targetType(); if (oppositeInfo == null) { - throw new IllegalArgumentException("Opposite %s of %s is not defined" + throw new TranslationException(linkType, "Opposite %s of %s is not defined" .formatted(opposite, linkType)); } if (!linkType.equals(oppositeInfo.opposite())) { - throw new IllegalArgumentException("Expected %s to have opposite %s, got %s instead" + throw new TranslationException(opposite, "Expected %s to have opposite %s, got %s instead" .formatted(opposite, linkType, oppositeInfo.opposite())); } if (!targetType.equals(oppositeInfo.sourceType())) { - throw new IllegalArgumentException("Expected %s to have source type %s, got %s instead" + throw new TranslationException(linkType, "Expected %s to have source type %s, got %s instead" .formatted(opposite, targetType, oppositeInfo.sourceType())); } if (!sourceType.equals(oppositeInfo.targetType())) { - throw new IllegalArgumentException("Expected %s to have target type %s, got %s instead" + throw new TranslationException(linkType, "Expected %s to have target type %s, got %s instead" .formatted(opposite, sourceType, oppositeInfo.targetType())); } if (oppositeInfo.containment() && info.containment()) { - throw new IllegalArgumentException("Opposite %s of containment %s cannot be containment" + throw new TranslationException(opposite, "Opposite %s of containment %s cannot be containment" .formatted(opposite, linkType)); } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiobject/MultiObjectInitializer.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiobject/MultiObjectInitializer.java index fb84631d..f11ab46b 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiobject/MultiObjectInitializer.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiobject/MultiObjectInitializer.java @@ -10,6 +10,7 @@ import tools.refinery.store.model.Model; import tools.refinery.store.reasoning.ReasoningAdapter; import tools.refinery.store.reasoning.refinement.PartialModelInitializer; import tools.refinery.store.reasoning.seed.ModelSeed; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.representation.Symbol; import tools.refinery.store.representation.TruthValue; import tools.refinery.store.representation.cardinality.CardinalityInterval; @@ -37,7 +38,8 @@ class MultiObjectInitializer implements PartialModelInitializer { for (int i = 0; i < intervals.length; i++) { var interval = intervals[i]; if (interval.isEmpty()) { - throw new IllegalArgumentException("Inconsistent existence or equality for node " + i); + throw new TranslationException(ReasoningAdapter.EXISTS_SYMBOL, + "Inconsistent existence or equality for node " + i); } var uniqueInterval = uniqueTable.computeIfAbsent(intervals[i], Function.identity()); countInterpretation.put(Tuple.of(i), uniqueInterval); @@ -58,9 +60,10 @@ class MultiObjectInitializer implements PartialModelInitializer { } else { Arrays.fill(intervals, CardinalityIntervals.SET); if (!modelSeed.containsSeed(ReasoningAdapter.EXISTS_SYMBOL) || - !modelSeed.containsSeed(ReasoningAdapter.EQUALS_SYMBOL)) { - throw new IllegalArgumentException("Seed for %s and %s is required if there is no seed for %s" - .formatted(ReasoningAdapter.EXISTS_SYMBOL, ReasoningAdapter.EQUALS_SYMBOL, + !modelSeed.containsSeed(ReasoningAdapter.EQUALS_SYMBOL)) { + throw new TranslationException(MultiObjectTranslator.COUNT_SYMBOL, + "Seed for %s and %s is required if there is no seed for %s".formatted( + ReasoningAdapter.EXISTS_SYMBOL, ReasoningAdapter.EQUALS_SYMBOL, MultiObjectTranslator.COUNT_SYMBOL)); } } @@ -78,9 +81,10 @@ class MultiObjectInitializer implements PartialModelInitializer { switch (cursor.getValue()) { case TRUE -> intervals[i] = intervals[i].meet(CardinalityIntervals.SOME); case FALSE -> intervals[i] = intervals[i].meet(CardinalityIntervals.NONE); - case ERROR -> throw new IllegalArgumentException("Inconsistent existence for node " + i); - default -> throw new IllegalArgumentException("Invalid existence truth value %s for node %d" - .formatted(cursor.getValue(), i)); + case ERROR -> throw new TranslationException(ReasoningAdapter.EXISTS_SYMBOL, + "Inconsistent existence for node " + i); + default -> throw new TranslationException(ReasoningAdapter.EXISTS_SYMBOL, + "Invalid existence truth value %s for node %d".formatted(cursor.getValue(), i)); } } } @@ -96,8 +100,8 @@ class MultiObjectInitializer implements PartialModelInitializer { int i = key.get(0); int otherIndex = key.get(1); if (i != otherIndex) { - throw new IllegalArgumentException("Off-diagonal equivalence (%d, %d) is not permitted" - .formatted(i, otherIndex)); + throw new TranslationException(ReasoningAdapter.EQUALS_SYMBOL, + "Off-diagonal equivalence (%d, %d) is not permitted".formatted(i, otherIndex)); } checkNodeId(intervals, i); switch (cursor.getValue()) { @@ -105,14 +109,15 @@ class MultiObjectInitializer implements PartialModelInitializer { case UNKNOWN -> { // Nothing do to, {@code intervals} is initialized with unknown equality. } - case ERROR -> throw new IllegalArgumentException("Inconsistent equality for node " + i); - default -> throw new IllegalArgumentException("Invalid equality truth value %s for node %d" - .formatted(cursor.getValue(), i)); + case ERROR -> throw new TranslationException(ReasoningAdapter.EQUALS_SYMBOL, + "Inconsistent equality for node " + i); + default -> throw new TranslationException(ReasoningAdapter.EQUALS_SYMBOL, + "Invalid equality truth value %s for node %d".formatted(cursor.getValue(), i)); } } for (int i = 0; i < intervals.length; i++) { if (seed.get(Tuple.of(i, i)) == TruthValue.FALSE) { - throw new IllegalArgumentException("Inconsistent equality for node " + i); + throw new TranslationException(ReasoningAdapter.EQUALS_SYMBOL, "Inconsistent equality for node " + i); } } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiplicity/ConstrainedMultiplicity.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiplicity/ConstrainedMultiplicity.java index e441e41e..9db9cc96 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiplicity/ConstrainedMultiplicity.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiplicity/ConstrainedMultiplicity.java @@ -6,6 +6,7 @@ package tools.refinery.store.reasoning.translator.multiplicity; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.representation.cardinality.CardinalityInterval; import tools.refinery.store.representation.cardinality.CardinalityIntervals; import tools.refinery.store.representation.cardinality.NonEmptyCardinalityInterval; @@ -14,17 +15,17 @@ public record ConstrainedMultiplicity(NonEmptyCardinalityInterval multiplicity, implements Multiplicity { public ConstrainedMultiplicity { if (multiplicity.equals(CardinalityIntervals.SET)) { - throw new IllegalArgumentException("Expected a constrained cardinality interval"); + throw new TranslationException(errorSymbol, "Expected a constrained cardinality interval"); } if (errorSymbol.arity() != 1) { - throw new IllegalArgumentException("Expected error symbol %s to have arity 1, got %d instead" + throw new TranslationException(errorSymbol, "Expected error symbol %s to have arity 1, got %d instead" .formatted(errorSymbol, errorSymbol.arity())); } } public static ConstrainedMultiplicity of(CardinalityInterval multiplicity, PartialRelation errorSymbol) { if (!(multiplicity instanceof NonEmptyCardinalityInterval nonEmptyCardinalityInterval)) { - throw new IllegalArgumentException("Inconsistent multiplicity"); + throw new TranslationException(errorSymbol, "Inconsistent multiplicity"); } return new ConstrainedMultiplicity(nonEmptyCardinalityInterval, errorSymbol); } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiplicity/InvalidMultiplicityErrorTranslator.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiplicity/InvalidMultiplicityErrorTranslator.java index 522d8455..c5e5e83e 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiplicity/InvalidMultiplicityErrorTranslator.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiplicity/InvalidMultiplicityErrorTranslator.java @@ -14,6 +14,7 @@ import tools.refinery.store.reasoning.lifting.DnfLifter; import tools.refinery.store.reasoning.literal.*; import tools.refinery.store.reasoning.representation.PartialRelation; import tools.refinery.store.reasoning.translator.PartialRelationTranslator; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.representation.cardinality.FiniteUpperCardinality; import tools.refinery.store.representation.cardinality.UpperCardinalities; import tools.refinery.store.representation.cardinality.UpperCardinality; @@ -36,11 +37,11 @@ public class InvalidMultiplicityErrorTranslator implements ModelStoreConfigurati public InvalidMultiplicityErrorTranslator(PartialRelation nodeType, PartialRelation linkType, boolean inverse, Multiplicity multiplicity) { if (nodeType.arity() != 1) { - throw new IllegalArgumentException("Node type must be of arity 1, got %s with arity %d instead" + throw new TranslationException(linkType, "Node type must be of arity 1, got %s with arity %d instead" .formatted(nodeType, nodeType.arity())); } if (linkType.arity() != 2) { - throw new IllegalArgumentException("Link type must be of arity 2, got %s with arity %d instead" + throw new TranslationException(linkType, "Link type must be of arity 2, got %s with arity %d instead" .formatted(linkType, linkType.arity())); } this.nodeType = nodeType; diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/opposite/OppositeRelationTranslator.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/opposite/OppositeRelationTranslator.java index b25b9d7d..6e15a628 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/opposite/OppositeRelationTranslator.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/opposite/OppositeRelationTranslator.java @@ -17,6 +17,7 @@ import tools.refinery.store.reasoning.literal.Modality; import tools.refinery.store.reasoning.refinement.RefinementBasedInitializer; import tools.refinery.store.reasoning.representation.PartialRelation; import tools.refinery.store.reasoning.translator.PartialRelationTranslator; +import tools.refinery.store.reasoning.translator.TranslationException; import java.util.List; import java.util.Set; @@ -26,6 +27,16 @@ public class OppositeRelationTranslator implements ModelStoreConfiguration, Part private final PartialRelation opposite; public OppositeRelationTranslator(PartialRelation linkType, PartialRelation opposite) { + if (linkType.arity() != 2) { + throw new TranslationException(linkType, + "Expected relation with opposite %s to have arity 2, got %d instead" + .formatted(linkType, linkType.arity())); + } + if (opposite.arity() != 2) { + throw new TranslationException(linkType, + "Expected opposite %s of %s to have arity 2, got %d instead" + .formatted(opposite, linkType, opposite.arity())); + } this.linkType = linkType; this.opposite = opposite; } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/predicate/PredicateTranslator.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/predicate/PredicateTranslator.java index ee022f2d..16745da1 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/predicate/PredicateTranslator.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/predicate/PredicateTranslator.java @@ -17,6 +17,7 @@ import tools.refinery.store.query.view.MayView; import tools.refinery.store.query.view.MustView; import tools.refinery.store.reasoning.representation.PartialRelation; import tools.refinery.store.reasoning.translator.PartialRelationTranslator; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.representation.Symbol; import tools.refinery.store.representation.TruthValue; @@ -33,11 +34,11 @@ public class PredicateTranslator implements ModelStoreConfiguration { public PredicateTranslator(PartialRelation relation, RelationalQuery query, boolean mutable, TruthValue defaultValue) { if (relation.arity() != query.arity()) { - throw new IllegalArgumentException("Expected arity %d query for partial relation %s, got %d instead" + throw new TranslationException(relation, "Expected arity %d query for partial relation %s, got %d instead" .formatted(relation.arity(), relation, query.arity())); } if (defaultValue.must()) { - throw new IllegalArgumentException("Default value must be UNKNOWN or FALSE"); + throw new TranslationException(relation, "Default value must be UNKNOWN or FALSE"); } this.relation = relation; this.query = query; diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchy.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchy.java index 35ec54ad..3f918c97 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchy.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchy.java @@ -6,6 +6,7 @@ package tools.refinery.store.reasoning.translator.typehierarchy; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.TranslationException; import java.util.*; @@ -81,8 +82,9 @@ public class TypeHierarchy { for (var supertype : allSupertypes) { var supertypeInfo = extendedTypeInfoMap.get(supertype); if (supertypeInfo == null) { - throw new IllegalArgumentException("Supertype %s of %s is missing from the type hierarchy" - .formatted(supertype, extendedTypeInfo.getType())); + throw new TranslationException(extendedTypeInfo.getType(), + "Supertype %s of %s is missing from the type hierarchy" + .formatted(supertype, extendedTypeInfo.getType())); } found.addAll(supertypeInfo.getAllSupertypes()); } @@ -101,7 +103,7 @@ public class TypeHierarchy { } for (var supertype : extendedTypeInfo.getAllSupertypes()) { if (type.equals(supertype)) { - throw new IllegalArgumentException("%s cannot be a supertype of itself".formatted(type)); + throw new TranslationException(type, "%s cannot be a supertype of itself".formatted(type)); } var supertypeInfo = extendedTypeInfoMap.get(supertype); supertypeInfo.getAllSubtypes().add(type); diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyBuilder.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyBuilder.java index 36efb878..ce8fda05 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyBuilder.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyBuilder.java @@ -6,6 +6,7 @@ package tools.refinery.store.reasoning.translator.typehierarchy; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.TranslationException; import java.util.*; @@ -18,12 +19,14 @@ public class TypeHierarchyBuilder { public TypeHierarchyBuilder type(PartialRelation partialRelation, TypeInfo typeInfo) { if (partialRelation.arity() != 1) { - throw new IllegalArgumentException("Only types of arity 1 are supported, got %s with %d instead" - .formatted(partialRelation, partialRelation.arity())); + throw new TranslationException(partialRelation, + "Only types of arity 1 are supported, got %s with %d instead" + .formatted(partialRelation, partialRelation.arity())); } var putResult = typeInfoMap.put(partialRelation, typeInfo); if (putResult != null && !putResult.equals(typeInfo)) { - throw new IllegalArgumentException("Duplicate type info for partial relation: " + partialRelation); + throw new TranslationException(partialRelation, + "Duplicate type info for partial relation: " + partialRelation); } return this; } diff --git a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilderTest.java b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilderTest.java index 115ba8cd..0f1a1006 100644 --- a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilderTest.java +++ b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilderTest.java @@ -7,6 +7,7 @@ package tools.refinery.store.reasoning.translator.metamodel; import org.junit.jupiter.api.Test; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.reasoning.translator.multiplicity.ConstrainedMultiplicity; import tools.refinery.store.representation.cardinality.CardinalityIntervals; @@ -26,7 +27,7 @@ class MetamodelBuilderTest { .reference(courses, university, course, location) .reference(location, course, university); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(TranslationException.class, builder::build); } @Test @@ -37,7 +38,7 @@ class MetamodelBuilderTest { .reference(courses, university, course, location) .reference(location, course, course, courses); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(TranslationException.class, builder::build); } @Test @@ -52,6 +53,6 @@ class MetamodelBuilderTest { ConstrainedMultiplicity.of(CardinalityIntervals.atLeast(2), invalidMultiplicity), university, courses); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(TranslationException.class, builder::build); } } diff --git a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyTest.java b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyTest.java index 9fbf2334..931c62dd 100644 --- a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyTest.java +++ b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyTest.java @@ -8,6 +8,7 @@ package tools.refinery.store.reasoning.translator.typehierarchy; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.representation.TruthValue; import java.util.Set; @@ -200,7 +201,7 @@ class TypeHierarchyTest { .type(c1, c2) .type(c2, c1); - assertThrows(IllegalArgumentException.class, builder::build); + assertThrows(TranslationException.class, builder::build); } @Test -- cgit v1.2.3-54-g00ecf From 0e54d399424374d497d08a8631c4761dece57ceb Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Wed, 23 Aug 2023 03:36:25 +0200 Subject: feat: dot visualization --- .../patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch | 82 ++ .../frontend/config/graphvizUMDVitePlugin.ts | 63 ++ subprojects/frontend/package.json | 13 +- subprojects/frontend/src/editor/EditorStore.ts | 5 +- subprojects/frontend/src/graph/GraphArea.tsx | 318 +++++++ subprojects/frontend/src/graph/GraphPane.tsx | 30 +- subprojects/frontend/src/xtext/SemanticsService.ts | 12 +- .../frontend/src/xtext/xtextServiceResults.ts | 17 +- subprojects/frontend/vite.config.ts | 2 + .../language/web/SecurityHeadersFilter.java | 4 +- yarn.lock | 917 +++++++++++++++++++-- 11 files changed, 1389 insertions(+), 74 deletions(-) create mode 100644 .yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch create mode 100644 subprojects/frontend/config/graphvizUMDVitePlugin.ts create mode 100644 subprojects/frontend/src/graph/GraphArea.tsx (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch b/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch new file mode 100644 index 00000000..161db0d7 --- /dev/null +++ b/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch @@ -0,0 +1,82 @@ +diff --git a/src/dot.js b/src/dot.js +index 96ae02b6edd947ac9086f3108986c08d91470cba..c4422b08d73f7fe73dc52ad905cf981d1f3cbcc5 100644 +--- a/src/dot.js ++++ b/src/dot.js +@@ -1,4 +1,3 @@ +-import { Graphviz } from "@hpcc-js/wasm/graphviz"; + import * as d3 from "d3-selection"; + import {extractAllElementsData, extractElementData, createElementWithAttributes} from "./element.js"; + import {convertToPathData} from "./svg.js"; +@@ -8,31 +7,6 @@ import {getEdgeTitle} from "./data.js"; + + + export function initViz() { +- +- // force JIT compilation of @hpcc-js/wasm +- try { +- Graphviz.load().then(graphviz => { +- graphviz.layout("", "svg", "dot"); +- this.layoutSync = graphviz.layout.bind(graphviz); +- if (this._worker == null) { +- this._dispatch.call("initEnd", this); +- } +- if (this._afterInit) { +- this._afterInit(); +- } +- }); +-// after the port to ESM modules, we don't know how to trigger this so +-// we just disable it from coverage +-/* c8 ignore start */ +- } catch(error) { +- // we end up here when the the script tag type used to load +- // the "@hpcc-js/wasm" script is not "application/javascript" +- // or "text/javascript", but typically "javascript/worker". In +- // this case the browser does not load the script since it's +- // unnecessary because it's loaded by the web worker +- // instead. This is expected so we just ignore the error. +- } +-/* c8 ignore stop */ + if (this._worker != null) { + var vizURL = this._vizURL; + var graphvizInstance = this; +@@ -337,7 +311,8 @@ function layoutDone(svgDoc, callback) { + + var newSvg = newDoc + .select('svg'); +- ++ ++ this._dispatch.call('postProcessSVG', this, newSvg); + var data = extractAllElementsData(newSvg); + this._dispatch.call('dataExtractEnd', this); + postProcessDataPass1Local(data); +diff --git a/src/graphviz.js b/src/graphviz.js +index c4638cb0e4042844c59c52dfe4749e13999fef6e..28dcfb71ad787c78645c460a29e9c52295c5f6bf 100644 +--- a/src/graphviz.js ++++ b/src/graphviz.js +@@ -49,7 +49,6 @@ import {drawnNodeSelection} from "./drawNode.js"; + import {workerCode} from "./workerCode.js"; + import {sharedWorkerCode} from "./workerCode.js"; + import {workerCodeBody} from "./workerCode.js"; +-import {Graphviz as hpccWasmGraphviz} from "@hpcc-js/wasm/graphviz"; + + export function Graphviz(selection, options) { + this._options = { +@@ -119,10 +118,6 @@ export function Graphviz(selection, options) { + this._workerPort = this._worker; + this._workerPortClose = this._worker.terminate.bind(this._worker); + this._workerCallbacks = []; +- } else { +- hpccWasmGraphviz.load().then(((graphviz) => { +- this._graphvizVersion = graphviz.version(); +- }).bind(this)); + } + this._selection = selection; + this._active = false; +@@ -143,6 +138,7 @@ export function Graphviz(selection, options) { + 'start', + 'layoutStart', + 'layoutEnd', ++ 'postProcessSVG', + 'dataExtractEnd', + 'dataProcessPass1End', + 'dataProcessPass2End', diff --git a/subprojects/frontend/config/graphvizUMDVitePlugin.ts b/subprojects/frontend/config/graphvizUMDVitePlugin.ts new file mode 100644 index 00000000..7a42560b --- /dev/null +++ b/subprojects/frontend/config/graphvizUMDVitePlugin.ts @@ -0,0 +1,63 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import pnpapi from 'pnpapi'; +import type { PluginOption, ResolvedConfig } from 'vite'; + +// Use a CJS file as the PnP resolution issuer to force resolution to a non-ESM export. +const issuerFileName = 'worker.cjs'; + +export default function graphvizUMDVitePlugin(): PluginOption { + let command: ResolvedConfig['command'] = 'build'; + let root: string | undefined; + let url: string | undefined; + + return { + name: 'graphviz-umd', + enforce: 'post', + configResolved(config) { + ({ command, root } = config); + }, + async buildStart() { + const issuer = + root === undefined ? issuerFileName : path.join(issuerFileName); + const resolvedPath = pnpapi.resolveRequest( + '@hpcc-js/wasm/graphviz', + issuer, + ); + if (resolvedPath === null) { + return; + } + if (command === 'serve') { + url = `/@fs/${resolvedPath}`; + } else { + const content = await readFile(resolvedPath, null); + url = this.emitFile({ + name: path.basename(resolvedPath), + type: 'asset', + source: content, + }); + } + }, + renderStart() { + if (url !== undefined && command !== 'serve') { + url = this.getFileName(url); + } + }, + transformIndexHtml() { + if (url === undefined) { + return undefined; + } + return [ + { + tag: 'script', + attrs: { + src: url, + type: 'javascript/worker', + }, + injectTo: 'head', + }, + ]; + }, + }; +} diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index 39ebd1df..9df8d85f 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json @@ -39,15 +39,19 @@ "@emotion/styled": "^11.11.0", "@fontsource-variable/inter": "^5.0.8", "@fontsource-variable/jetbrains-mono": "^5.0.9", + "@hpcc-js/wasm": "^2.13.1", "@lezer/common": "^1.0.3", "@lezer/highlight": "^1.1.6", "@lezer/lr": "^1.3.9", "@material-icons/svg": "^1.0.33", "@mui/icons-material": "5.14.3", "@mui/material": "5.14.5", - "@vitejs/plugin-react-swc": "^3.3.2", "ansi-styles": "^6.2.1", "csstype": "^3.1.2", + "d3": "^7.8.5", + "d3-graphviz": "patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", "escape-string-regexp": "^5.0.0", "json-stringify-pretty-compact": "^4.0.0", "lodash-es": "^4.17.21", @@ -66,17 +70,23 @@ }, "devDependencies": { "@lezer/generator": "^1.4.0", + "@types/d3": "^7.4.0", + "@types/d3-graphviz": "^2.6.7", + "@types/d3-selection": "^3.0.5", + "@types/d3-zoom": "^3.0.4", "@types/eslint": "^8.44.2", "@types/html-minifier-terser": "^7.0.0", "@types/lodash-es": "^4.17.8", "@types/micromatch": "^4.0.2", "@types/ms": "^0.7.31", "@types/node": "^20.5.0", + "@types/pnpapi": "^0.0.2", "@types/prettier": "^3.0.0", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", + "@vitejs/plugin-react-swc": "^3.3.2", "@xstate/cli": "^0.5.2", "cross-env": "^7.0.3", "eslint": "^8.47.0", @@ -92,6 +102,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "html-minifier-terser": "^7.2.0", "micromatch": "^4.0.5", + "pnpapi": "^0.0.0", "prettier": "^3.0.1", "typescript": "5.1.6", "vite": "^4.4.9", diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index 563725bb..10f01099 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts @@ -28,6 +28,7 @@ import { nanoid } from 'nanoid'; import type PWAStore from '../PWAStore'; import getLogger from '../utils/getLogger'; import type XtextClient from '../xtext/XtextClient'; +import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; import EditorErrors from './EditorErrors'; import LintPanelStore from './LintPanelStore'; @@ -65,7 +66,7 @@ export default class EditorStore { semanticsError: string | undefined; - semantics: unknown = {}; + semantics: SemanticsSuccessResult | undefined; constructor(initialValue: string, pwaStore: PWAStore) { this.id = nanoid(); @@ -295,7 +296,7 @@ export default class EditorStore { this.semanticsError = semanticsError; } - setSemantics(semantics: unknown) { + setSemantics(semantics: SemanticsSuccessResult) { this.semanticsError = undefined; this.semantics = semantics; } diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx new file mode 100644 index 00000000..b55245d8 --- /dev/null +++ b/subprojects/frontend/src/graph/GraphArea.tsx @@ -0,0 +1,318 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import Box from '@mui/material/Box'; +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 { useCallback, useRef, useState } from 'react'; +import { useResizeDetector } from 'react-resize-detector'; + +import { useRootStore } from '../RootStoreProvider'; +import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; + +function toGraphviz( + semantics: SemanticsSuccessResult | undefined, +): string | undefined { + if (semantics === undefined) { + return undefined; + } + const lines = [ + 'digraph {', + 'graph [bgcolor=transparent];', + 'node [fontsize=16, shape=plain];', + 'edge [fontsize=12, color=black];', + ]; + 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'); +} + +interface Transform { + x: number; + y: number; + k: number; +} + +export default function GraphArea(): JSX.Element { + const { editorStore } = useRootStore(); + const disposerRef = useRef(); + const graphvizRef = useRef< + Graphviz | undefined + >(); + const canvasRef = useRef(); + const zoomRef = useRef< + d3.ZoomBehavior | undefined + >(); + const [zoom, setZoom] = useState({ x: 0, y: 0, k: 1 }); + const widthRef = useRef(); + const heightRef = useRef(); + + const onResize = useCallback( + (width: number | undefined, height: number | undefined) => { + if (canvasRef.current === undefined || zoomRef.current === undefined) { + return; + } + let moveX = 0; + let moveY = 0; + if (widthRef.current !== undefined && width !== undefined) { + moveX = (width - widthRef.current) / 2; + } + if (heightRef.current !== undefined && height !== undefined) { + moveY = (height - heightRef.current) / 2; + } + widthRef.current = width; + heightRef.current = height; + if (moveX === 0 && moveY === 0) { + return; + } + const currentTransform = d3.zoomTransform(canvasRef.current); + zoomRef.current.translateBy( + d3.select(canvasRef.current), + moveX / currentTransform.k - moveX, + moveY / currentTransform.k - moveY, + ); + }, + [], + ); + + const { ref: setCanvasResize } = useResizeDetector({ + onResize, + }); + + const setCanvas = useCallback( + (element: HTMLDivElement | null) => { + canvasRef.current = element ?? undefined; + setCanvasResize(element); + if (element === null) { + return; + } + const zoomBehavior = d3.zoom(); + zoomBehavior.on( + 'zoom', + (event: d3.D3ZoomEvent) => + setZoom(event.transform), + ); + d3.select(element).call(zoomBehavior); + zoomRef.current = zoomBehavior; + }, + [setCanvasResize], + ); + + const setElement = useCallback( + (element: HTMLDivElement | null) => { + if (disposerRef.current !== undefined) { + disposerRef.current(); + disposerRef.current = undefined; + } + if (graphvizRef.current !== undefined) { + // `@types/d3-graphviz` does not contain the signature for the `destroy` method. + (graphvizRef.current as unknown as { destroy(): void }).destroy(); + graphvizRef.current = undefined; + } + if (element !== null) { + element.replaceChildren(); + const renderer = graphviz(element) as Graphviz< + BaseType, + unknown, + null, + undefined + >; + renderer.keyMode('id'); + renderer.zoom(false); + renderer.tweenPrecision('5%'); + renderer.tweenShapes(false); + renderer.convertEqualSidedPolygons(false); + const transition = () => + d3.transition().duration(300).ease(d3.easeCubic); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument, + @typescript-eslint/no-explicit-any -- + Workaround for error in `@types/d3-graphviz`. + */ + renderer.transition(transition as any); + renderer.on( + 'postProcessSVG', + // @ts-expect-error Custom `d3-graphviz` hook not covered by typings. + ( + svgSelection: Selection, + ) => { + svgSelection.selectAll('title').remove(); + const svg = svgSelection.node(); + if (svg === null) { + return; + } + svg.querySelectorAll('.node').forEach((node) => { + node.querySelectorAll('path').forEach((path) => { + const d = path.getAttribute('d') ?? ''; + const points = d.split(/[A-Z ]/); + points.shift(); + const x = points.map((p) => { + return Number(p.split(',')[0] ?? 0); + }); + const y = points.map((p) => { + return Number(p.split(',')[1] ?? 0); + }); + const xmin = Math.min.apply(null, x); + const xmax = Math.max.apply(null, x); + const ymin = Math.min.apply(null, y); + const ymax = Math.max.apply(null, y); + const rect = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'rect', + ); + rect.setAttribute('fill', path.getAttribute('fill') ?? ''); + rect.setAttribute('stroke', path.getAttribute('stroke') ?? ''); + rect.setAttribute('x', String(xmin)); + rect.setAttribute('y', String(ymin)); + rect.setAttribute('width', String(xmax - xmin)); + rect.setAttribute('height', String(ymax - ymin)); + rect.setAttribute('height', String(ymax - ymin)); + rect.setAttribute('rx', '12'); + rect.setAttribute('ry', '12'); + node.replaceChild(rect, path); + }); + }); + }, + ); + disposerRef.current = reaction( + () => editorStore?.semantics, + (semantics) => { + const str = toGraphviz(semantics); + if (str !== undefined) { + renderer.renderDot(str); + } + }, + { fireImmediately: true }, + ); + graphvizRef.current = renderer; + } + }, + [editorStore], + ); + + return ( + ({ + width: '100%', + height: '100%', + position: 'relative', + overflow: 'hidden', + '& svg': { + userSelect: 'none', + '& .node': { + '& text': { + ...theme.typography.body2, + fill: theme.palette.text.primary, + }, + '& [stroke="black"]': { + stroke: theme.palette.text.primary, + }, + '& [fill="green"]': { + fill: + theme.palette.mode === 'dark' + ? theme.palette.primary.dark + : theme.palette.primary.light, + }, + '& [fill="white"]': { + fill: theme.palette.background.default, + stroke: theme.palette.background.default, + }, + }, + '& .edge': { + '& text': { + ...theme.typography.caption, + fill: theme.palette.text.primary, + }, + '& [stroke="black"]': { + stroke: theme.palette.text.primary, + }, + '& [fill="black"]': { + fill: theme.palette.text.primary, + }, + }, + '& .edge-UNKNOWN': { + '& text': { + fill: theme.palette.text.secondary, + }, + '& [stroke="black"]': { + stroke: theme.palette.text.secondary, + }, + '& [fill="black"]': { + fill: theme.palette.text.secondary, + }, + }, + '& .edge-ERROR': { + '& text': { + fill: theme.palette.error.main, + }, + '& [stroke="black"]': { + stroke: theme.palette.error.main, + }, + '& [fill="black"]': { + fill: theme.palette.error.main, + }, + }, + }, + })} + ref={setCanvas} + > + + + ); +} diff --git a/subprojects/frontend/src/graph/GraphPane.tsx b/subprojects/frontend/src/graph/GraphPane.tsx index f69f52a6..f04b9931 100644 --- a/subprojects/frontend/src/graph/GraphPane.tsx +++ b/subprojects/frontend/src/graph/GraphPane.tsx @@ -5,24 +5,24 @@ */ 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 { Suspense, lazy } from 'react'; -import { useRootStore } from '../RootStoreProvider'; +import Loading from '../Loading'; -const StyledCode = styled('code')(({ theme }) => ({ - ...theme.typography.editor, - fontWeight: theme.typography.fontWeightEditorNormal, - margin: theme.spacing(2), - whiteSpace: 'pre', -})); +const GraphArea = lazy(() => import('./GraphArea')); -export default observer(function GraphPane(): JSX.Element { - const { editorStore } = useRootStore(); +export default function GraphPane(): JSX.Element { return ( - - {stringify(editorStore?.semantics ?? {})} + + }> + + ); -}); +} diff --git a/subprojects/frontend/src/xtext/SemanticsService.ts b/subprojects/frontend/src/xtext/SemanticsService.ts index 50ec371a..d68b87a9 100644 --- a/subprojects/frontend/src/xtext/SemanticsService.ts +++ b/subprojects/frontend/src/xtext/SemanticsService.ts @@ -17,11 +17,15 @@ export default class SemanticsService { onPush(push: unknown): void { const result = SemanticsResult.parse(push); - this.validationService.setSemanticsIssues(result.issues ?? []); - if (result.error !== undefined) { - this.store.setSemanticsError(result.error); + if ('issues' in result) { + this.validationService.setSemanticsIssues(result.issues); } else { - this.store.setSemantics(push); + this.validationService.setSemanticsIssues([]); + if ('error' in result) { + this.store.setSemanticsError(result.error); + } else { + this.store.setSemantics(result); + } } this.store.analysisCompleted(); } diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index cae95771..12f87b26 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts @@ -126,9 +126,20 @@ export const FormattingResult = DocumentStateResult.extend({ export type FormattingResult = z.infer; -export const SemanticsResult = z.object({ - error: z.string().optional(), - issues: Issue.array().optional(), +export const SemanticsSuccessResult = z.object({ + nodes: z.string().nullable().array(), + partialInterpretation: z.record( + z.string(), + z.union([z.number(), z.string()]).array().array(), + ), }); +export type SemanticsSuccessResult = z.infer; + +export const SemanticsResult = z.union([ + z.object({ error: z.string() }), + z.object({ issues: Issue.array() }), + SemanticsSuccessResult, +]); + export type SemanticsResult = z.infer; diff --git a/subprojects/frontend/vite.config.ts b/subprojects/frontend/vite.config.ts index 1104f867..5bda8071 100644 --- a/subprojects/frontend/vite.config.ts +++ b/subprojects/frontend/vite.config.ts @@ -17,6 +17,7 @@ import detectDevModeOptions, { API_ENDPOINT, } from './config/detectDevModeOptions'; import fetchPackageMetadata from './config/fetchPackageMetadata'; +import graphvizUMDVitePlugin from './config/graphvizUMDVitePlugin'; import manifest from './config/manifest'; import minifyHTMLVitePlugin from './config/minifyHTMLVitePlugin'; import preloadFontsVitePlugin from './config/preloadFontsVitePlugin'; @@ -43,6 +44,7 @@ const viteConfig: ViteConfig = { lezer(), preloadFontsVitePlugin(fontsGlob), minifyHTMLVitePlugin(), + graphvizUMDVitePlugin(), VitePWA({ strategies: 'generateSW', registerType: 'prompt', diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java index 7b094fde..fab94689 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java @@ -16,7 +16,7 @@ public class SecurityHeadersFilter implements Filter { ServletException { if (response instanceof HttpServletResponse httpResponse) { httpResponse.setHeader("Content-Security-Policy", "default-src 'none'; " + - "script-src 'self'; " + + "script-src 'self' 'wasm-unsafe-eval'; " + // CodeMirror needs inline styles, see e.g., // https://discuss.codemirror.net/t/inline-styles-and-content-security-policy/1311/2 "style-src 'self' 'unsafe-inline'; " + @@ -25,7 +25,7 @@ public class SecurityHeadersFilter implements Filter { "font-src 'self'; " + "connect-src 'self'; " + "manifest-src 'self'; " + - "worker-src 'self';"); + "worker-src 'self' blob:;"); httpResponse.setHeader("X-Content-Type-Options", "nosniff"); httpResponse.setHeader("X-Frame-Options", "DENY"); httpResponse.setHeader("Referrer-Policy", "strict-origin"); diff --git a/yarn.lock b/yarn.lock index bb806e86..b1d7b9d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,6 +1722,17 @@ __metadata: languageName: node linkType: hard +"@hpcc-js/wasm@npm:2.13.1, @hpcc-js/wasm@npm:^2.13.1": + version: 2.13.1 + resolution: "@hpcc-js/wasm@npm:2.13.1" + dependencies: + yargs: "npm:17.7.2" + bin: + dot-wasm: bin/dot-wasm.js + checksum: 3ed818f52ad8d9c671abbcdd7553af30ce0c7775b7f2c48997a7a7180d3719c61516972d25aa4942d947b55127257c6137ec0c142b693a87cb235ba1407fc2ed + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.10": version: 0.11.10 resolution: "@humanwhocodes/config-array@npm:0.11.10" @@ -2108,6 +2119,7 @@ __metadata: "@emotion/styled": "npm:^11.11.0" "@fontsource-variable/inter": "npm:^5.0.8" "@fontsource-variable/jetbrains-mono": "npm:^5.0.9" + "@hpcc-js/wasm": "npm:^2.13.1" "@lezer/common": "npm:^1.0.3" "@lezer/generator": "npm:^1.4.0" "@lezer/highlight": "npm:^1.1.6" @@ -2115,12 +2127,17 @@ __metadata: "@material-icons/svg": "npm:^1.0.33" "@mui/icons-material": "npm:5.14.3" "@mui/material": "npm:5.14.5" + "@types/d3": "npm:^7.4.0" + "@types/d3-graphviz": "npm:^2.6.7" + "@types/d3-selection": "npm:^3.0.5" + "@types/d3-zoom": "npm:^3.0.4" "@types/eslint": "npm:^8.44.2" "@types/html-minifier-terser": "npm:^7.0.0" "@types/lodash-es": "npm:^4.17.8" "@types/micromatch": "npm:^4.0.2" "@types/ms": "npm:^0.7.31" "@types/node": "npm:^20.5.0" + "@types/pnpapi": "npm:^0.0.2" "@types/prettier": "npm:^3.0.0" "@types/react": "npm:^18.2.20" "@types/react-dom": "npm:^18.2.7" @@ -2131,6 +2148,10 @@ __metadata: ansi-styles: "npm:^6.2.1" cross-env: "npm:^7.0.3" csstype: "npm:^3.1.2" + d3: "npm:^7.8.5" + d3-graphviz: "patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" escape-string-regexp: "npm:^5.0.0" eslint: "npm:^8.47.0" eslint-config-airbnb: "npm:^19.0.4" @@ -2154,6 +2175,7 @@ __metadata: ms: "npm:^2.1.3" nanoid: "npm:^4.0.2" notistack: "npm:^3.0.1" + pnpapi: "npm:^0.0.0" prettier: "npm:^3.0.1" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" @@ -2246,90 +2268,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-darwin-arm64@npm:1.3.64" +"@swc/core-darwin-arm64@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-darwin-arm64@npm:1.3.78" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-darwin-x64@npm:1.3.64" +"@swc/core-darwin-x64@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-darwin-x64@npm:1.3.78" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.64" +"@swc/core-linux-arm-gnueabihf@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.78" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.64" +"@swc/core-linux-arm64-gnu@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.78" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.64" +"@swc/core-linux-arm64-musl@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.78" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.64" +"@swc/core-linux-x64-gnu@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.78" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-linux-x64-musl@npm:1.3.64" +"@swc/core-linux-x64-musl@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-linux-x64-musl@npm:1.3.78" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.64" +"@swc/core-win32-arm64-msvc@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.78" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.64" +"@swc/core-win32-ia32-msvc@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.78" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.64": - version: 1.3.64 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.64" +"@swc/core-win32-x64-msvc@npm:1.3.78": + version: 1.3.78 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.78" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.61": - version: 1.3.64 - resolution: "@swc/core@npm:1.3.64" - dependencies: - "@swc/core-darwin-arm64": "npm:1.3.64" - "@swc/core-darwin-x64": "npm:1.3.64" - "@swc/core-linux-arm-gnueabihf": "npm:1.3.64" - "@swc/core-linux-arm64-gnu": "npm:1.3.64" - "@swc/core-linux-arm64-musl": "npm:1.3.64" - "@swc/core-linux-x64-gnu": "npm:1.3.64" - "@swc/core-linux-x64-musl": "npm:1.3.64" - "@swc/core-win32-arm64-msvc": "npm:1.3.64" - "@swc/core-win32-ia32-msvc": "npm:1.3.64" - "@swc/core-win32-x64-msvc": "npm:1.3.64" + version: 1.3.78 + resolution: "@swc/core@npm:1.3.78" + dependencies: + "@swc/core-darwin-arm64": "npm:1.3.78" + "@swc/core-darwin-x64": "npm:1.3.78" + "@swc/core-linux-arm-gnueabihf": "npm:1.3.78" + "@swc/core-linux-arm64-gnu": "npm:1.3.78" + "@swc/core-linux-arm64-musl": "npm:1.3.78" + "@swc/core-linux-x64-gnu": "npm:1.3.78" + "@swc/core-linux-x64-musl": "npm:1.3.78" + "@swc/core-win32-arm64-msvc": "npm:1.3.78" + "@swc/core-win32-ia32-msvc": "npm:1.3.78" + "@swc/core-win32-x64-msvc": "npm:1.3.78" peerDependencies: "@swc/helpers": ^0.5.0 dependenciesMeta: @@ -2356,7 +2378,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: a860405e71cf217c72d0520da14b0b2fd31767d51167283139fd5788512a2dbedda2f89145d1f008060d57f2b59c88c0b30a1d4698ed5daba7a6c0b725943d52 + checksum: 5936575f2dd8ef18642d4d83622336b0bc2ea073d4554ad20e4af030329b301ad9e7f5f4afa4042ac8d3cffc9d2d5c3140e7d134869e7c656d5e29231ffbc17f languageName: node linkType: hard @@ -2374,6 +2396,341 @@ __metadata: languageName: node linkType: hard +"@types/d3-array@npm:*": + version: 3.0.5 + resolution: "@types/d3-array@npm:3.0.5" + checksum: 145c61ffc88af9ac550d924e2d13a8b6fc95011989662500848e2df2214e7b3e19decaf7e95238a2c5460053137227360ccd00f6859c4ecb2508a807338ab957 + languageName: node + linkType: hard + +"@types/d3-axis@npm:*": + version: 3.0.2 + resolution: "@types/d3-axis@npm:3.0.2" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 3efaca6b227fff21eea654b980952e7315dbe5822956d48c6c6047a815adf100b31a901c1cd0e089067f8a409abae2728cfcb81b9da28c02aee96b009237d6e7 + languageName: node + linkType: hard + +"@types/d3-brush@npm:*": + version: 3.0.2 + resolution: "@types/d3-brush@npm:3.0.2" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 8ddc4978fd5ef637ddc459a7a26b2b14e59a19cb2c541904ec17005e6d3fc3cea426598b8dd08ca154dc1cf0fe466fef3bf344b3b5bc2d9591d48783f8e76a96 + languageName: node + linkType: hard + +"@types/d3-chord@npm:*": + version: 3.0.2 + resolution: "@types/d3-chord@npm:3.0.2" + checksum: e354f29b261d4ff9546e52e1c3e315e62407a8ead85c82bd7e4efb277e89898ae2fa1a7740589e15d32677d76c826a596b198d374749e27598f9d3dec0055d7f + languageName: node + linkType: hard + +"@types/d3-color@npm:*": + version: 3.1.0 + resolution: "@types/d3-color@npm:3.1.0" + checksum: 5b4be21b4b025da9ffd0cef876fb7d82f99116fa26e7ee3449771faf0a953d160246b1ceb2a9bbc7d131e32ab60d7d19013131d098616369a56f9880f25f20ef + languageName: node + linkType: hard + +"@types/d3-color@npm:^1": + version: 1.4.2 + resolution: "@types/d3-color@npm:1.4.2" + checksum: f1c70d7deabe2b30e337361c3fa26b7ce4e9c875dfd1ad75e17379c6a5596f116fb21ca0e595c2f2220e04866bca6bdb2623e772cfb5f8bf4d20ffe6ba5c72ab + languageName: node + linkType: hard + +"@types/d3-contour@npm:*": + version: 3.0.2 + resolution: "@types/d3-contour@npm:3.0.2" + dependencies: + "@types/d3-array": "npm:*" + "@types/geojson": "npm:*" + checksum: b4fc2b783c944b35412080c873ec828896864144675db9376b390c2a64631f5a63939fdefd50bf8f3cf0cb80ac8113429334f303b7806a71dd3155e1f762a304 + languageName: node + linkType: hard + +"@types/d3-delaunay@npm:*": + version: 6.0.1 + resolution: "@types/d3-delaunay@npm:6.0.1" + checksum: b03f84560a98e0d08b96095759484de6ebccc4fc137a9114795ece15898ccb67c5b0897ffe1e939658224fe387dd58090b951a2c3ff31c70ec9fe2dddc0df1f9 + languageName: node + linkType: hard + +"@types/d3-dispatch@npm:*": + version: 3.0.2 + resolution: "@types/d3-dispatch@npm:3.0.2" + checksum: 4d3afa2ff31abe7207347f7bdf044b5a94ba39935670a745d1599021e56f7f29eb0e09685cb32519d9ee45ec9d0de865ac4c77ed6c1e049cb1d820b5a1085c09 + languageName: node + linkType: hard + +"@types/d3-drag@npm:*": + version: 3.0.2 + resolution: "@types/d3-drag@npm:3.0.2" + dependencies: + "@types/d3-selection": "npm:*" + checksum: b8af2eaf78df1acce61bc70c8684fc97be3b2824d096107a93e34c157f3e680d9635aeb6227431c1b226808442a55e728109615690a0ff240479eb04216afb94 + languageName: node + linkType: hard + +"@types/d3-dsv@npm:*": + version: 3.0.1 + resolution: "@types/d3-dsv@npm:3.0.1" + checksum: 1daac684901a4b4022213bfd82cb0b2917c950cb2f1d0d925aabe2aab88c7cfdef67f522a08259e6c19d7c54fb98e4a8a994d9e48d8fb8d5bcda813969c5afc6 + languageName: node + linkType: hard + +"@types/d3-ease@npm:*": + version: 3.0.0 + resolution: "@types/d3-ease@npm:3.0.0" + checksum: 8fa64035f3b459cbf178e0bbb01cd188ec7337877e959fcf0a6ef08528b6caf93fd9f69635ec1c8fc11f6d2448d0e5d2a4e11988cb16bc6e001f0c0afe609204 + languageName: node + linkType: hard + +"@types/d3-fetch@npm:*": + version: 3.0.2 + resolution: "@types/d3-fetch@npm:3.0.2" + dependencies: + "@types/d3-dsv": "npm:*" + checksum: ee592cb03b0651b882851d022df6fc17934ab955d642b9178c2e1e800cd7e75733bde02a1f4936cff1befcec918556144a3525bd0cc6a3c8c5446de4a8bf4bf7 + languageName: node + linkType: hard + +"@types/d3-force@npm:*": + version: 3.0.4 + resolution: "@types/d3-force@npm:3.0.4" + checksum: d1c1c00742f55c8f8d99e0fa09be990ae4dc562d2fd1861d86dfdc69efbcc47e1e8a30a97cde64a6c0175dcc8c2cfa1c7ab4c021063f96a13182e14a2d0c5ff1 + languageName: node + linkType: hard + +"@types/d3-format@npm:*": + version: 3.0.1 + resolution: "@types/d3-format@npm:3.0.1" + checksum: abfb57288fb26899bac98aeb03d652ed73d7074c12c96bb33d2d67ced9869f52f4b3e37579df883fc69a13d970199331a07e67a8bcd9f858efeb4f3a71e36188 + languageName: node + linkType: hard + +"@types/d3-geo@npm:*": + version: 3.0.3 + resolution: "@types/d3-geo@npm:3.0.3" + dependencies: + "@types/geojson": "npm:*" + checksum: f1409f60507644b331a2355e54e3ff497011cb0e1b4faa5962c6ee9f1f5e80f4ca9a400c67c8c7bea2b3e67c3f7684a047c91330d2d0216ddb63b3ddc9ac8f96 + languageName: node + linkType: hard + +"@types/d3-graphviz@npm:^2.6.7": + version: 2.6.7 + resolution: "@types/d3-graphviz@npm:2.6.7" + dependencies: + "@types/d3-selection": "npm:^1" + "@types/d3-transition": "npm:^1" + "@types/d3-zoom": "npm:^1" + checksum: 5584a0126bf3baebd92f7cc4082430f554a9a3026e5c2320f1bd9560b62e62cc1f52cf56d96da97b8fb669ab187fbf376509dc8ef359b870004aae905f4289be + languageName: node + linkType: hard + +"@types/d3-hierarchy@npm:*": + version: 3.1.2 + resolution: "@types/d3-hierarchy@npm:3.1.2" + checksum: 9248d1d01f659e30808da58171652542d88d4e07364dac4acffcf3513509b26d5e2971880f56e29091cf89b0d2f8de64fcd9cb86017d9192d0f27e863c965414 + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:*": + version: 3.0.1 + resolution: "@types/d3-interpolate@npm:3.0.1" + dependencies: + "@types/d3-color": "npm:*" + checksum: 1c7577045a4a30dc177bca10980c456a28c9b89b1a5406fe7303824dd9cc898f67f8dafd8e22a7702ca5df12a28a5f48f77d92a9b5d8f1fc0939f33831067114 + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:^1": + version: 1.4.2 + resolution: "@types/d3-interpolate@npm:1.4.2" + dependencies: + "@types/d3-color": "npm:^1" + checksum: 98bff93ce4d94485a4f6117e554854ec69072382910008e785d2c960b50e643093a8cfa2e0875b3d1dff19f3603b6e16a4eea8122c7c8ead3623daf3044cd22e + languageName: node + linkType: hard + +"@types/d3-path@npm:*": + version: 3.0.0 + resolution: "@types/d3-path@npm:3.0.0" + checksum: 64750aeb3e490112d2f1d812230201140de352743846150e022e44c6924f44d47deb1a50f3dc63b930fd6a8fd6482f8fcb7da2516a14b8e29a4749d2b86f90ca + languageName: node + linkType: hard + +"@types/d3-polygon@npm:*": + version: 3.0.0 + resolution: "@types/d3-polygon@npm:3.0.0" + checksum: 6fce6672581cac3c5bcda6f0014527228bdc25ad9f99d1bf9103764f6ce89bc0d5690db55c92052ad7182fa20623aa4bb6bcb2b85aa7a77535610c06b3c32d97 + languageName: node + linkType: hard + +"@types/d3-quadtree@npm:*": + version: 3.0.2 + resolution: "@types/d3-quadtree@npm:3.0.2" + checksum: 0a2a6921bb21d8bd14190bfd048990f80d2369e622822cbc344a897bb88534b1d7332445024e17cf0adfb4cef663a8a79f0e3bf2a4129a7d13f264854f06e379 + languageName: node + linkType: hard + +"@types/d3-random@npm:*": + version: 3.0.1 + resolution: "@types/d3-random@npm:3.0.1" + checksum: e2818c15b157df55f48c50ca1ed8ba243859b56eb0eb07fdca162ea34ef77f373e5fd5402df4c2e483d6a71e0b57b97ce189cb9398d0433569be6318e1ede408 + languageName: node + linkType: hard + +"@types/d3-scale-chromatic@npm:*": + version: 3.0.0 + resolution: "@types/d3-scale-chromatic@npm:3.0.0" + checksum: 58cef12deab19233d8d06f61993198930248fb7cf5de0754e38a9dd342a8fba2d917bc746b57eeee9f82e50bbc079a867e15cc045e8a338cf8502ebcde4561f9 + languageName: node + linkType: hard + +"@types/d3-scale@npm:*": + version: 4.0.3 + resolution: "@types/d3-scale@npm:4.0.3" + dependencies: + "@types/d3-time": "npm:*" + checksum: 5eace4cf45f87b3eec9637ade77e97530e778a3bb7f8356e4712bde732fb9474f3e8ef3aa12bc97dd3e4f76e23343ed81c1f5a3a1dcfdb72868f876b418da117 + languageName: node + linkType: hard + +"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.5": + version: 3.0.5 + resolution: "@types/d3-selection@npm:3.0.5" + checksum: 431b1f65ee4e4eb4c8a3b4ddebb00b00c04ce082e68bffd7bc790e600f55c296d875bf073ae8a09de27499a33bed38635cdb57a9e4f5d3fc948fe956c2ec84cb + languageName: node + linkType: hard + +"@types/d3-selection@npm:^1": + version: 1.4.3 + resolution: "@types/d3-selection@npm:1.4.3" + checksum: 47c181f8362ade4df151e01737816356c939bc5728ff87c3a29bd43aaa0413296170119949eb3aa0ef8d9c10fac4463eb1d154b6fd0e89617b45eeb06bdefb8b + languageName: node + linkType: hard + +"@types/d3-shape@npm:*": + version: 3.1.1 + resolution: "@types/d3-shape@npm:3.1.1" + dependencies: + "@types/d3-path": "npm:*" + checksum: e06f0e6f5d74184dfb6a71861085ffad221bde8a11d2de632649118d75e9605fddf9af664601b0841d794e0c27afd6ea37d652350fb47c196905facc04c284d5 + languageName: node + linkType: hard + +"@types/d3-time-format@npm:*": + version: 4.0.0 + resolution: "@types/d3-time-format@npm:4.0.0" + checksum: 3b2b95950594ded5bb6c1b21da37b049daa945c93a93ced76eac2ea6d78d6d17ebabbcf226523b07e214fe53c5d0f98f5d2e9ce7c758bc29c25e3563afddcf87 + languageName: node + linkType: hard + +"@types/d3-time@npm:*": + version: 3.0.0 + resolution: "@types/d3-time@npm:3.0.0" + checksum: 4f900608d5c557b09b38e4b096723af5eb4508a1b32f9afae253fe77a4bcbbe821a14225bab1c2ea36ddbc5c4154ab3563452c6b6eba7a9f48cefad94276e6b5 + languageName: node + linkType: hard + +"@types/d3-timer@npm:*": + version: 3.0.0 + resolution: "@types/d3-timer@npm:3.0.0" + checksum: 7f6cd693f6c99a360dc01e1b5aa1185cfa8f65d603f537c52c810d475c8ef8aa07ac2f8be24cc489d2e69b843e384ab30dd079ac75011dbc91b21cd216a79502 + languageName: node + linkType: hard + +"@types/d3-transition@npm:*": + version: 3.0.3 + resolution: "@types/d3-transition@npm:3.0.3" + dependencies: + "@types/d3-selection": "npm:*" + checksum: b91742816f4f8b16c26b8a41065f0b72170b5eb9f5a9af30c7e432ff0b7b78a02fb7228d4e4bb8471e91d8122aa96dd8f8e879ceb181cc006039e87a0c696fbf + languageName: node + linkType: hard + +"@types/d3-transition@npm:^1": + version: 1.3.2 + resolution: "@types/d3-transition@npm:1.3.2" + dependencies: + "@types/d3-selection": "npm:^1" + checksum: 9e1340c2840fde63f224550cbde5531b8b2493218d421f97de138c1e7ca9b1f481dfc9e4c91cb5ce84df9df671e2d95468556be1f8cf1e28f0c33929322115a4 + languageName: node + linkType: hard + +"@types/d3-zoom@npm:*": + version: 3.0.3 + resolution: "@types/d3-zoom@npm:3.0.3" + dependencies: + "@types/d3-interpolate": "npm:*" + "@types/d3-selection": "npm:*" + checksum: 7b48870cadf18ba1104613605fc6157dc230f71ecac7f2f0d7e689b44a62ab29377edf98e935a6eee8b87c32456effe3cf6aab321cc20bb31695d21f62fdb311 + languageName: node + linkType: hard + +"@types/d3-zoom@npm:^1": + version: 1.8.4 + resolution: "@types/d3-zoom@npm:1.8.4" + dependencies: + "@types/d3-interpolate": "npm:^1" + "@types/d3-selection": "npm:^1" + checksum: 3135d2e01ec2eb633a5104025e719d989cdd9d203eeb97fc749128d60c2d19de9693e058e92ea1eb2f63aa0fa47cd4e4a994dc7df614b00eda650952c92f2c8a + languageName: node + linkType: hard + +"@types/d3-zoom@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/d3-zoom@npm:3.0.4" + dependencies: + "@types/d3-interpolate": "npm:*" + "@types/d3-selection": "npm:*" + checksum: 52d1b5f2a1490c25c69b03dcd71601f908ea1e3dedf79e379be4ff23e3a6be85d0694d04c1b23686343a74117e86683a61433ed926637aea545268fd55aa634f + languageName: node + linkType: hard + +"@types/d3@npm:^7.4.0": + version: 7.4.0 + resolution: "@types/d3@npm:7.4.0" + dependencies: + "@types/d3-array": "npm:*" + "@types/d3-axis": "npm:*" + "@types/d3-brush": "npm:*" + "@types/d3-chord": "npm:*" + "@types/d3-color": "npm:*" + "@types/d3-contour": "npm:*" + "@types/d3-delaunay": "npm:*" + "@types/d3-dispatch": "npm:*" + "@types/d3-drag": "npm:*" + "@types/d3-dsv": "npm:*" + "@types/d3-ease": "npm:*" + "@types/d3-fetch": "npm:*" + "@types/d3-force": "npm:*" + "@types/d3-format": "npm:*" + "@types/d3-geo": "npm:*" + "@types/d3-hierarchy": "npm:*" + "@types/d3-interpolate": "npm:*" + "@types/d3-path": "npm:*" + "@types/d3-polygon": "npm:*" + "@types/d3-quadtree": "npm:*" + "@types/d3-random": "npm:*" + "@types/d3-scale": "npm:*" + "@types/d3-scale-chromatic": "npm:*" + "@types/d3-selection": "npm:*" + "@types/d3-shape": "npm:*" + "@types/d3-time": "npm:*" + "@types/d3-time-format": "npm:*" + "@types/d3-timer": "npm:*" + "@types/d3-transition": "npm:*" + "@types/d3-zoom": "npm:*" + checksum: f382cb3c78257f77cf4a1f19279bbe46eeb1b8df3ef5ab58edba8e18ce7afda670e79dde91fb8c2b85565ad5d0b138e62909477f3e6452b9d586050ca3e605d7 + languageName: node + linkType: hard + "@types/eslint@npm:^8.44.2": version: 8.44.2 resolution: "@types/eslint@npm:8.44.2" @@ -2398,6 +2755,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:*": + version: 7946.0.10 + resolution: "@types/geojson@npm:7946.0.10" + checksum: 4abba554467494c1496a60622c2cb6f8c7f80b0dbb909ff898812a9f67799fd1b254802d7d266361974bd8b0c9ef32a7686183aac83b20c437f6d0eee89cd0a1 + languageName: node + linkType: hard + "@types/html-minifier-terser@npm:^7.0.0": version: 7.0.0 resolution: "@types/html-minifier-terser@npm:7.0.0" @@ -2465,6 +2829,13 @@ __metadata: languageName: node linkType: hard +"@types/pnpapi@npm:^0.0.2": + version: 0.0.2 + resolution: "@types/pnpapi@npm:0.0.2" + checksum: 4851d742f44dd7d682b27956aec0444a6837114cc9b32fc2830d4cd32959bede3a18c18c659ea0549fdc3e18f2e2d4c1bea08383b1a72f4689491471836fad48 + languageName: node + linkType: hard + "@types/prettier@npm:^3.0.0": version: 3.0.0 resolution: "@types/prettier@npm:3.0.0" @@ -3285,6 +3656,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 + languageName: node + linkType: hard + "clsx@npm:^1.1.0": version: 1.2.1 resolution: "clsx@npm:1.2.1" @@ -3340,6 +3722,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:7": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a + languageName: node + linkType: hard + "commander@npm:^10.0.0": version: 10.0.1 resolution: "commander@npm:10.0.1" @@ -3462,6 +3851,360 @@ __metadata: languageName: node linkType: hard +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3, d3-array@npm:3, d3-array@npm:^3.2.0": + version: 3.2.4 + resolution: "d3-array@npm:3.2.4" + dependencies: + internmap: "npm:1 - 2" + checksum: 08b95e91130f98c1375db0e0af718f4371ccacef7d5d257727fe74f79a24383e79aba280b9ffae655483ffbbad4fd1dec4ade0119d88c4749f388641c8bf8c50 + languageName: node + linkType: hard + +"d3-axis@npm:3": + version: 3.0.0 + resolution: "d3-axis@npm:3.0.0" + checksum: a271e70ba1966daa5aaf6a7f959ceca3e12997b43297e757c7b945db2e1ead3c6ee226f2abcfa22abbd4e2e28bd2b71a0911794c4e5b911bbba271328a582c78 + languageName: node + linkType: hard + +"d3-brush@npm:3": + version: 3.0.0 + resolution: "d3-brush@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:3" + d3-transition: "npm:3" + checksum: 07baf00334c576da2f68a91fc0da5732c3a5fa19bd3d7aed7fd24d1d674a773f71a93e9687c154176f7246946194d77c48c2d8fed757f5dcb1a4740067ec50a8 + languageName: node + linkType: hard + +"d3-chord@npm:3": + version: 3.0.1 + resolution: "d3-chord@npm:3.0.1" + dependencies: + d3-path: "npm:1 - 3" + checksum: baa6013914af3f4fe1521f0d16de31a38eb8a71d08ff1dec4741f6f45a828661e5cd3935e39bd14e3032bdc78206c283ca37411da21d46ec3cfc520be6e7a7ce + languageName: node + linkType: hard + +"d3-color@npm:1 - 3, d3-color@npm:3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c + languageName: node + linkType: hard + +"d3-contour@npm:4": + version: 4.0.2 + resolution: "d3-contour@npm:4.0.2" + dependencies: + d3-array: "npm:^3.2.0" + checksum: 98bc5fbed6009e08707434a952076f39f1cd6ed8b9288253cc3e6a3286e4e80c63c62d84954b20e64bf6e4ededcc69add54d3db25e990784a59c04edd3449032 + languageName: node + linkType: hard + +"d3-delaunay@npm:6": + version: 6.0.4 + resolution: "d3-delaunay@npm:6.0.4" + dependencies: + delaunator: "npm:5" + checksum: 57c3aecd2525664b07c4c292aa11cf49b2752c0cf3f5257f752999399fe3c592de2d418644d79df1f255471eec8057a9cc0c3062ed7128cb3348c45f69597754 + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3, d3-dispatch@npm:3, d3-dispatch@npm:^3.0.1": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: 6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3, d3-drag@npm:3": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-selection: "npm:3" + checksum: d2556e8dc720741a443b595a30af403dd60642dfd938d44d6e9bfc4c71a962142f9a028c56b61f8b4790b65a34acad177d1263d66f103c3c527767b0926ef5aa + languageName: node + linkType: hard + +"d3-dsv@npm:1 - 3, d3-dsv@npm:3": + version: 3.0.1 + resolution: "d3-dsv@npm:3.0.1" + dependencies: + commander: "npm:7" + iconv-lite: "npm:0.6" + rw: "npm:1" + bin: + csv2json: bin/dsv2json.js + csv2tsv: bin/dsv2dsv.js + dsv2dsv: bin/dsv2dsv.js + dsv2json: bin/dsv2json.js + json2csv: bin/json2dsv.js + json2dsv: bin/json2dsv.js + json2tsv: bin/json2dsv.js + tsv2csv: bin/dsv2dsv.js + tsv2json: bin/dsv2json.js + checksum: 10e6af9e331950ed258f34ab49ac1b7060128ef81dcf32afc790bd1f7e8c3cc2aac7f5f875250a83f21f39bb5925fbd0872bb209f8aca32b3b77d32bab8a65ab + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3, d3-ease@npm:3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0 + languageName: node + linkType: hard + +"d3-fetch@npm:3": + version: 3.0.1 + resolution: "d3-fetch@npm:3.0.1" + dependencies: + d3-dsv: "npm:1 - 3" + checksum: 4f467a79bf290395ac0cbb5f7562483f6a18668adc4c8eb84c9d3eff048b6f6d3b6f55079ba1ebf1908dabe000c941d46be447f8d78453b2dad5fb59fb6aa93b + languageName: node + linkType: hard + +"d3-force@npm:3": + version: 3.0.0 + resolution: "d3-force@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-quadtree: "npm:1 - 3" + d3-timer: "npm:1 - 3" + checksum: 220a16a1a1ac62ba56df61028896e4b52be89c81040d20229c876efc8852191482c233f8a52bb5a4e0875c321b8e5cb6413ef3dfa4d8fe79eeb7d52c587f52cf + languageName: node + linkType: hard + +"d3-format@npm:1 - 3, d3-format@npm:3, d3-format@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-format@npm:3.1.0" + checksum: 049f5c0871ebce9859fc5e2f07f336b3c5bfff52a2540e0bac7e703fce567cd9346f4ad1079dd18d6f1e0eaa0599941c1810898926f10ac21a31fd0a34b4aa75 + languageName: node + linkType: hard + +"d3-geo@npm:3": + version: 3.1.0 + resolution: "d3-geo@npm:3.1.0" + dependencies: + d3-array: "npm:2.5.0 - 3" + checksum: 5b0a26d232787ca9e824a660827c28626a51004328dde7c76a1bd300d3cad8c7eeb55fea64c8cd6495d5a34fea434fb1418d59926a6cb24e6fb6e2b6f62c6bd9 + languageName: node + linkType: hard + +"d3-graphviz@npm:5.1.0": + version: 5.1.0 + resolution: "d3-graphviz@npm:5.1.0" + dependencies: + "@hpcc-js/wasm": "npm:2.13.1" + d3-dispatch: "npm:^3.0.1" + d3-format: "npm:^3.1.0" + d3-interpolate: "npm:^3.0.1" + d3-path: "npm:^3.1.0" + d3-timer: "npm:^3.0.1" + d3-transition: "npm:^3.0.1" + d3-zoom: "npm:^3.0.0" + peerDependencies: + d3-selection: ^3.0.0 + checksum: 81eab7e0ba13e095a16f0accf6aeef6cbc0d3839fbd5cb4ea08ae6b279dabe3555a535d07edf57d33a78e606a02ead45f4362f2bb3ddcbf9c703e61fd95353f2 + languageName: node + linkType: hard + +"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" + dependencies: + "@hpcc-js/wasm": "npm:2.13.1" + d3-dispatch: "npm:^3.0.1" + d3-format: "npm:^3.1.0" + d3-interpolate: "npm:^3.0.1" + d3-path: "npm:^3.1.0" + d3-timer: "npm:^3.0.1" + d3-transition: "npm:^3.0.1" + d3-zoom: "npm:^3.0.0" + peerDependencies: + d3-selection: ^3.0.0 + checksum: 23e56b979950ff19f12321e9c23e56e55e791950f42ced3613581f4ac6a70e7b78b4bf3c600377df0766ee20f967741c939011b7a4d192a9eb3e2e07fa45833d + languageName: node + linkType: hard + +"d3-hierarchy@npm:3": + version: 3.1.2 + resolution: "d3-hierarchy@npm:3.1.2" + checksum: 6dcdb480539644aa7fc0d72dfc7b03f99dfbcdf02714044e8c708577e0d5981deb9d3e99bbbb2d26422b55bcc342ac89a0fa2ea6c9d7302e2fc0951dd96f89cf + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:3, d3-interpolate@npm:^3.0.1": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + checksum: 19f4b4daa8d733906671afff7767c19488f51a43d251f8b7f484d5d3cfc36c663f0a66c38fe91eee30f40327443d799be17169f55a293a3ba949e84e57a33e6a + languageName: node + linkType: hard + +"d3-path@npm:1 - 3, d3-path@npm:3, d3-path@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-path@npm:3.1.0" + checksum: dc1d58ec87fa8319bd240cf7689995111a124b141428354e9637aa83059eb12e681f77187e0ada5dedfce346f7e3d1f903467ceb41b379bfd01cd8e31721f5da + languageName: node + linkType: hard + +"d3-polygon@npm:3": + version: 3.0.1 + resolution: "d3-polygon@npm:3.0.1" + checksum: e236aa7f33efa9a4072907af7dc119f85b150a0716759d4fe5f12f62573018264a6cbde8617fbfa6944a7ae48c1c0c8d3f39ae72e11f66dd471e9b5e668385df + languageName: node + linkType: hard + +"d3-quadtree@npm:1 - 3, d3-quadtree@npm:3": + version: 3.0.1 + resolution: "d3-quadtree@npm:3.0.1" + checksum: 18302d2548bfecaef788152397edec95a76400fd97d9d7f42a089ceb68d910f685c96579d74e3712d57477ed042b056881b47cd836a521de683c66f47ce89090 + languageName: node + linkType: hard + +"d3-random@npm:3": + version: 3.0.1 + resolution: "d3-random@npm:3.0.1" + checksum: 987a1a1bcbf26e6cf01fd89d5a265b463b2cea93560fc17d9b1c45e8ed6ff2db5924601bcceb808de24c94133f000039eb7fa1c469a7a844ccbf1170cbb25b41 + languageName: node + linkType: hard + +"d3-scale-chromatic@npm:3": + version: 3.0.0 + resolution: "d3-scale-chromatic@npm:3.0.0" + dependencies: + d3-color: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + checksum: 920a80f2e31b5686798c116e99d1671c32f55fb60fa920b742aa4ac5175b878c615adb4e55a246d65367e6e1061fdbcc55807be731fb5b18ae628d1df62bfac1 + languageName: node + linkType: hard + +"d3-scale@npm:4": + version: 4.0.2 + resolution: "d3-scale@npm:4.0.2" + dependencies: + d3-array: "npm:2.10.0 - 3" + d3-format: "npm:1 - 3" + d3-interpolate: "npm:1.2.0 - 3" + d3-time: "npm:2.1.1 - 3" + d3-time-format: "npm:2 - 4" + checksum: 65d9ad8c2641aec30ed5673a7410feb187a224d6ca8d1a520d68a7d6eac9d04caedbff4713d1e8545be33eb7fec5739983a7ab1d22d4e5ad35368c6729d362f1 + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: e59096bbe8f0cb0daa1001d9bdd6dbc93a688019abc97d1d8b37f85cd3c286a6875b22adea0931b0c88410d025563e1643019161a883c516acf50c190a11b56b + languageName: node + linkType: hard + +"d3-shape@npm:3": + version: 3.2.0 + resolution: "d3-shape@npm:3.2.0" + dependencies: + d3-path: "npm:^3.1.0" + checksum: f1c9d1f09926daaf6f6193ae3b4c4b5521e81da7d8902d24b38694517c7f527ce3c9a77a9d3a5722ad1e3ff355860b014557b450023d66a944eabf8cfde37132 + languageName: node + linkType: hard + +"d3-time-format@npm:2 - 4, d3-time-format@npm:4": + version: 4.1.0 + resolution: "d3-time-format@npm:4.1.0" + dependencies: + d3-time: "npm:1 - 3" + checksum: 735e00fb25a7fd5d418fac350018713ae394eefddb0d745fab12bbff0517f9cdb5f807c7bbe87bb6eeb06249662f8ea84fec075f7d0cd68609735b2ceb29d206 + languageName: node + linkType: hard + +"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3": + version: 3.1.0 + resolution: "d3-time@npm:3.1.0" + dependencies: + d3-array: "npm:2 - 3" + checksum: a984f77e1aaeaa182679b46fbf57eceb6ebdb5f67d7578d6f68ef933f8eeb63737c0949991618a8d29472dbf43736c7d7f17c452b2770f8c1271191cba724ca1 + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3, d3-timer@npm:3, d3-timer@npm:^3.0.1": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3, d3-transition@npm:3, d3-transition@npm:^3.0.1": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + d3-dispatch: "npm:1 - 3" + d3-ease: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + d3-timer: "npm:1 - 3" + peerDependencies: + d3-selection: 2 - 3 + checksum: 4e74535dda7024aa43e141635b7522bb70cf9d3dfefed975eb643b36b864762eca67f88fafc2ca798174f83ca7c8a65e892624f824b3f65b8145c6a1a88dbbad + languageName: node + linkType: hard + +"d3-zoom@npm:3, d3-zoom@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:2 - 3" + d3-transition: "npm:2 - 3" + checksum: ee2036479049e70d8c783d594c444fe00e398246048e3f11a59755cd0e21de62ece3126181b0d7a31bf37bcf32fd726f83ae7dea4495ff86ec7736ce5ad36fd3 + languageName: node + linkType: hard + +"d3@npm:^7.8.5": + version: 7.8.5 + resolution: "d3@npm:7.8.5" + dependencies: + d3-array: "npm:3" + d3-axis: "npm:3" + d3-brush: "npm:3" + d3-chord: "npm:3" + d3-color: "npm:3" + d3-contour: "npm:4" + d3-delaunay: "npm:6" + d3-dispatch: "npm:3" + d3-drag: "npm:3" + d3-dsv: "npm:3" + d3-ease: "npm:3" + d3-fetch: "npm:3" + d3-force: "npm:3" + d3-format: "npm:3" + d3-geo: "npm:3" + d3-hierarchy: "npm:3" + d3-interpolate: "npm:3" + d3-path: "npm:3" + d3-polygon: "npm:3" + d3-quadtree: "npm:3" + d3-random: "npm:3" + d3-scale: "npm:4" + d3-scale-chromatic: "npm:3" + d3-selection: "npm:3" + d3-shape: "npm:3" + d3-time: "npm:3" + d3-time-format: "npm:4" + d3-timer: "npm:3" + d3-transition: "npm:3" + d3-zoom: "npm:3" + checksum: 408758dcc2437cbff8cd207b9d82760030b5c53c1df6a2ce5b1a76633388a6892fd65c0632cfa83da963e239722d49805062e5fb05d99e0fb078bda14cb22222 + languageName: node + linkType: hard + "damerau-levenshtein@npm:^1.0.8": version: 1.0.8 resolution: "damerau-levenshtein@npm:1.0.8" @@ -3521,6 +4264,15 @@ __metadata: languageName: node linkType: hard +"delaunator@npm:5": + version: 5.0.0 + resolution: "delaunator@npm:5.0.0" + dependencies: + robust-predicates: "npm:^3.0.0" + checksum: 8655c1ad12dc58bd6350f882c12065ea415cfc809e4cac12b7b5c4941e981aaabee1afdcf13985dcd545d13d0143eb3805836f50e2b097af8137b204dfbea4f6 + languageName: node + linkType: hard + "delegates@npm:^1.0.0": version: 1.0.0 resolution: "delegates@npm:1.0.0" @@ -4440,6 +5192,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1": version: 1.2.1 resolution: "get-intrinsic@npm:1.2.1" @@ -4742,7 +5501,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6, iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -4817,6 +5576,13 @@ __metadata: languageName: node linkType: hard +"internmap@npm:1 - 2": + version: 2.0.3 + resolution: "internmap@npm:2.0.3" + checksum: 8cedd57f07bbc22501516fbfc70447f0c6812871d471096fad9ea603516eacc2137b633633daf432c029712df0baefd793686388ddf5737e3ea15074b877f7ed + languageName: node + linkType: hard + "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -6010,6 +6776,13 @@ __metadata: languageName: node linkType: hard +"pnpapi@npm:^0.0.0": + version: 0.0.0 + resolution: "pnpapi@npm:0.0.0" + checksum: 7e718f0263073729486b35658fc502bc8f6f34e922da288731722fad5fa69f3817d29cce64d0135c41826c7f899999343daff5e49a9367f056ece9f1c1ded391 + languageName: node + linkType: hard + "postcss@npm:^8.4.27": version: 8.4.27 resolution: "postcss@npm:8.4.27" @@ -6283,6 +7056,13 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + "require-from-string@npm:^2.0.2": version: 2.0.2 resolution: "require-from-string@npm:2.0.2" @@ -6381,6 +7161,13 @@ __metadata: languageName: node linkType: hard +"robust-predicates@npm:^3.0.0": + version: 3.0.2 + resolution: "robust-predicates@npm:3.0.2" + checksum: 4ecd53649f1c2d49529c85518f2fa69ffb2f7a4453f7fd19c042421c7b4d76c3efb48bc1c740c8f7049346d7cb58cf08ee0c9adaae595cc23564d360adb1fde4 + languageName: node + linkType: hard + "rollup-plugin-terser@npm:^7.0.0": version: 7.0.2 resolution: "rollup-plugin-terser@npm:7.0.2" @@ -6432,6 +7219,13 @@ __metadata: languageName: node linkType: hard +"rw@npm:1": + version: 1.3.3 + resolution: "rw@npm:1.3.3" + checksum: b1e1ef37d1e79d9dc7050787866e30b6ddcb2625149276045c262c6b4d53075ddc35f387a856a8e76f0d0df59f4cd58fe24707e40797ebee66e542b840ed6a53 + languageName: node + linkType: hard + "safe-array-concat@npm:^1.0.0": version: 1.0.0 resolution: "safe-array-concat@npm:1.0.0" @@ -6646,7 +7440,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -7493,7 +8287,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -7529,6 +8323,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" @@ -7550,6 +8351,28 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 + languageName: node + linkType: hard + +"yargs@npm:17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" -- 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-web/src/main/java/tools') 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 cd96a9a4f54d45cda3ddf5df474946445d557090 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Sun, 3 Sep 2023 17:57:38 +0200 Subject: feat: scope propagator in language --- subprojects/frontend/src/editor/EditorTheme.ts | 2 +- subprojects/frontend/src/graph/dotSource.ts | 16 ++++- subprojects/language-semantics/build.gradle.kts | 1 + .../language/semantics/model/ModelInitializer.java | 69 ++++++++++++++++++++-- subprojects/language-web/build.gradle.kts | 1 + .../language/web/semantics/SemanticsWorker.java | 26 ++++++-- .../store/reasoning/scope/internal/MultiView.java | 23 ++++++++ .../scope/internal/ScopePropagatorAdapterImpl.java | 7 +-- .../scope/internal/ScopePropagatorBuilderImpl.java | 18 +----- .../store/reasoning/scope/MPSolverTest.java | 31 ++++++++++ .../cardinality/NonEmptyCardinalityInterval.java | 7 ++- 11 files changed, 165 insertions(+), 36 deletions(-) create mode 100644 subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/MultiView.java (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index 308d5be0..055b62e2 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts @@ -105,7 +105,7 @@ export default styled('div', { color: theme.palette.text.primary, }, }, - '.tok-problem-abstract, .tok-problem-new': { + '.tok-problem-abstract': { fontStyle: 'italic', }, '.tok-problem-containment': { diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts index 963a9663..b24bca2f 100644 --- a/subprojects/frontend/src/graph/dotSource.ts +++ b/subprojects/frontend/src/graph/dotSource.ts @@ -20,8 +20,6 @@ function nodeName(graph: GraphStore, metadata: NodeMetadata): string { switch (metadata.kind) { case 'INDIVIDUAL': return `<b>${name}</b>`; - case 'NEW': - return `<i>${name}</i>`; default: return name; } @@ -44,6 +42,7 @@ interface NodeData { exists: string; equalsSelf: string; unaryPredicates: Map<RelationMetadata, string>; + count: string; } function computeNodeData(graph: GraphStore): NodeData[] { @@ -56,6 +55,7 @@ function computeNodeData(graph: GraphStore): NodeData[] { exists: 'FALSE', equalsSelf: 'FALSE', unaryPredicates: new Map(), + count: '[0]', })); relations.forEach((relation) => { @@ -107,6 +107,15 @@ function computeNodeData(graph: GraphStore): NodeData[] { } }); + partialInterpretation['builtin::count']?.forEach(([index, value]) => { + if (typeof index === 'number' && typeof value === 'string') { + const data = nodeData[index]; + if (data !== undefined) { + data.count = value; + } + } + }); + return nodeData; } @@ -132,9 +141,10 @@ function createNodes(graph: GraphStore, lines: string[]): void { const classes = classList.join(' '); const name = nodeName(graph, node); const border = node.kind === 'INDIVIDUAL' ? 2 : 1; + const count = data.equalsSelf !== 'TRUE' ? ` ${data.count}` : ''; 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>`); + <tr><td cellpadding="4.5" width="32" bgcolor="green">${name}${count}</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">', diff --git a/subprojects/language-semantics/build.gradle.kts b/subprojects/language-semantics/build.gradle.kts index b6f3f709..4374f78c 100644 --- a/subprojects/language-semantics/build.gradle.kts +++ b/subprojects/language-semantics/build.gradle.kts @@ -14,5 +14,6 @@ dependencies { api(project(":refinery-store")) api(project(":refinery-store-query")) api(project(":refinery-store-reasoning")) + implementation(project(":refinery-store-reasoning-scope")) runtimeOnly(libs.eclipseCollections) } 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 13e25d0a..89c41a8e 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 @@ -24,6 +24,7 @@ import tools.refinery.store.query.term.Variable; import tools.refinery.store.reasoning.ReasoningAdapter; import tools.refinery.store.reasoning.representation.AnyPartialSymbol; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.scope.ScopePropagatorBuilder; import tools.refinery.store.reasoning.seed.ModelSeed; import tools.refinery.store.reasoning.seed.Seed; import tools.refinery.store.reasoning.translator.containment.ContainmentHierarchyTranslator; @@ -72,6 +73,8 @@ public class ModelInitializer { private Metamodel metamodel; + private Map<Tuple, CardinalityInterval> countSeed = new LinkedHashMap<>(); + private ModelSeed modelSeed; public Problem getProblem() { @@ -135,6 +138,10 @@ public class ModelInitializer { relationTrace.put(relation, partialRelation); modelSeedBuilder.seed(partialRelation, info.toSeed(nodeCount)); } + collectScopes(); + modelSeedBuilder.seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder + .reducedValue(CardinalityIntervals.SET) + .putAll(countSeed)); modelSeed = modelSeedBuilder.build(); collectPredicates(); return modelSeed; @@ -288,20 +295,26 @@ public class ModelInitializer { CardinalityInterval interval; if (problemMultiplicity == null) { interval = CardinalityIntervals.LONE; - } else if (problemMultiplicity instanceof ExactMultiplicity exactMultiplicity) { - interval = CardinalityIntervals.exactly(exactMultiplicity.getExactValue()); + } else { + interval = getCardinalityInterval(problemMultiplicity); + } + var constraint = getRelationInfo(referenceDeclaration.getInvalidMultiplicity()).partialRelation(); + return ConstrainedMultiplicity.of(interval, constraint); + } + + private static CardinalityInterval getCardinalityInterval( + tools.refinery.language.model.problem.Multiplicity problemMultiplicity) { + if (problemMultiplicity instanceof ExactMultiplicity exactMultiplicity) { + return CardinalityIntervals.exactly(exactMultiplicity.getExactValue()); } else if (problemMultiplicity instanceof RangeMultiplicity rangeMultiplicity) { var upperBound = rangeMultiplicity.getUpperBound(); - interval = CardinalityIntervals.between(rangeMultiplicity.getLowerBound(), + return CardinalityIntervals.between(rangeMultiplicity.getLowerBound(), upperBound < 0 ? UpperCardinalities.UNBOUNDED : UpperCardinalities.atMost(upperBound)); } else { throw new TracedException(problemMultiplicity, "Unknown multiplicity"); } - var constraint = getRelationInfo(referenceDeclaration.getInvalidMultiplicity()).partialRelation(); - return ConstrainedMultiplicity.of(interval, constraint); } - private void collectAssertions() { for (var statement : problem.getStatements()) { if (statement instanceof ClassDeclaration classDeclaration) { @@ -598,6 +611,50 @@ public class ModelInitializer { return argumentList; } + private void collectScopes() { + for (var statement : problem.getStatements()) { + if (statement instanceof ScopeDeclaration scopeDeclaration) { + for (var typeScope : scopeDeclaration.getTypeScopes()) { + if (typeScope.isIncrement()) { + collectTypeScopeIncrement(typeScope); + } else { + collectTypeScope(typeScope); + } + } + } + } + } + + private void collectTypeScopeIncrement(TypeScope typeScope) { + if (!(typeScope.getTargetType() instanceof ClassDeclaration classDeclaration)) { + throw new TracedException(typeScope, "Target of incremental type scope must be a class declaration"); + } + var newNode = classDeclaration.getNewNode(); + if (newNode == null) { + throw new TracedException(typeScope, "Target of incremental type scope must be concrete class"); + } + int newNodeId = nodeTrace.get(newNode); + var type = relationTrace.get(classDeclaration); + var typeInfo = metamodel.typeHierarchy().getAnalysisResult(type); + if (!typeInfo.getDirectSubtypes().isEmpty()) { + throw new TracedException(typeScope, "Target of incremental type scope cannot have any subclasses"); + } + var interval = getCardinalityInterval(typeScope.getMultiplicity()); + countSeed.compute(Tuple.of(newNodeId), (key, oldValue) -> + oldValue == null ? interval : oldValue.meet(interval)); + } + + private void collectTypeScope(TypeScope typeScope) { + var scopePropagatorBuilder = storeBuilder.tryGetAdapter(ScopePropagatorBuilder.class).orElseThrow( + () -> new TracedException(typeScope, "Type scopes require a ScopePropagatorBuilder")); + var type = relationTrace.get(typeScope.getTargetType()); + if (type == null) { + throw new TracedException(typeScope, "Unknown target type"); + } + var interval = getCardinalityInterval(typeScope.getMultiplicity()); + scopePropagatorBuilder.scope(type, interval); + } + private record RelationInfo(PartialRelation partialRelation, MutableSeed<TruthValue> assertions, MutableSeed<TruthValue> defaultAssertions) { public RelationInfo(String name, int arity, TruthValue value, TruthValue defaultValue) { diff --git a/subprojects/language-web/build.gradle.kts b/subprojects/language-web/build.gradle.kts index a4ccdd9f..9f772d41 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(project(":refinery-store-reasoning-scope")) implementation(libs.gson) implementation(libs.jetty.server) implementation(libs.jetty.servlet) 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 108b87dc..8470bb99 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 @@ -22,15 +22,18 @@ 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; +import tools.refinery.store.map.Cursor; 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.refinement.RefinementResult; import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.scope.ScopePropagatorAdapter; import tools.refinery.store.reasoning.translator.TranslationException; -import tools.refinery.store.representation.TruthValue; +import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; import tools.refinery.store.tuple.Tuple; import tools.refinery.viatra.runtime.CancellationToken; @@ -75,7 +78,8 @@ class SemanticsWorker implements Callable<SemanticsResult> { .with(ViatraModelQueryAdapter.builder() .cancellationToken(cancellationToken)) .with(ReasoningAdapter.builder() - .requiredInterpretations(Concreteness.PARTIAL)); + .requiredInterpretations(Concreteness.PARTIAL)) + .with(ScopePropagatorAdapter.builder()); cancellationToken.checkCancelled(); try { var modelSeed = initializer.createModel(problem, builder); @@ -90,6 +94,9 @@ class SemanticsWorker implements Callable<SemanticsResult> { cancellationToken.checkCancelled(); var cancellableModelSeed = CancellableSeed.wrap(cancellationToken, modelSeed); var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(cancellableModelSeed); + if (model.getAdapter(ScopePropagatorAdapter.class).propagate() == RefinementResult.REJECTED) { + return new SemanticsInternalErrorResult("Scopes are unsatisfiable"); + } cancellationToken.checkCancelled(); var partialInterpretation = getPartialInterpretation(initializer, model); @@ -113,13 +120,18 @@ class SemanticsWorker implements Callable<SemanticsResult> { json.add(name, tuples); cancellationToken.checkCancelled(); } + json.add("builtin::count", getCountJson(model)); 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<Tuple, TruthValue>(); + return getTuplesJson(cursor); + } + + private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) { + var map = new TreeMap<Tuple, Object>(); while (cursor.move()) { map.put(cursor.getKey(), cursor.getValue()); } @@ -130,7 +142,7 @@ class SemanticsWorker implements Callable<SemanticsResult> { return tuples; } - private static JsonArray toArray(Tuple tuple, TruthValue value) { + private static JsonArray toArray(Tuple tuple, Object value) { int arity = tuple.getSize(); var json = new JsonArray(arity + 1); for (int i = 0; i < arity; i++) { @@ -140,6 +152,12 @@ class SemanticsWorker implements Callable<SemanticsResult> { return json; } + private static JsonArray getCountJson(Model model) { + var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE); + var cursor = interpretation.getAll(); + return getTuplesJson(cursor); + } + private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) { if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) { return new SemanticsInternalErrorResult(message); diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/MultiView.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/MultiView.java new file mode 100644 index 00000000..cea4e07d --- /dev/null +++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/MultiView.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.reasoning.scope.internal; + +import tools.refinery.store.query.view.TuplePreservingView; +import tools.refinery.store.representation.Symbol; +import tools.refinery.store.representation.cardinality.CardinalityInterval; +import tools.refinery.store.representation.cardinality.CardinalityIntervals; +import tools.refinery.store.tuple.Tuple; + +class MultiView extends TuplePreservingView<CardinalityInterval> { + protected MultiView(Symbol<CardinalityInterval> symbol) { + super(symbol, "multi"); + } + + @Override + protected boolean doFilter(Tuple key, CardinalityInterval value) { + return !CardinalityIntervals.ONE.equals(value); + } +} diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/ScopePropagatorAdapterImpl.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/ScopePropagatorAdapterImpl.java index 0d594701..c257df6b 100644 --- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/ScopePropagatorAdapterImpl.java +++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/ScopePropagatorAdapterImpl.java @@ -210,10 +210,9 @@ class ScopePropagatorAdapterImpl implements ScopePropagatorAdapter { UpperCardinality upperBound; switch (maximizationResult) { case OPTIMAL -> upperBound = UpperCardinalities.atMost(RoundingUtil.roundDown(objective.value())); - case UNBOUNDED -> upperBound = UpperCardinalities.UNBOUNDED; - case INFEASIBLE -> { - return RefinementResult.REJECTED; - } + // Problem was feasible when minimizing, the only possible source of {@code UNBOUNDED_OR_INFEASIBLE} is + // an unbounded maximization problem. See https://github.com/google/or-tools/issues/3319 + case UNBOUNDED, INFEASIBLE -> upperBound = UpperCardinalities.UNBOUNDED; default -> throw new IllegalStateException("Failed to solve for maximum of %s: %s" .formatted(variable, minimizationResult)); } diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/ScopePropagatorBuilderImpl.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/ScopePropagatorBuilderImpl.java index f383ebeb..11ca7381 100644 --- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/ScopePropagatorBuilderImpl.java +++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/internal/ScopePropagatorBuilderImpl.java @@ -22,12 +22,6 @@ import tools.refinery.store.representation.cardinality.FiniteUpperCardinality; import java.util.*; -import static tools.refinery.store.query.literal.Literals.not; -import static tools.refinery.store.reasoning.ReasoningAdapter.EQUALS_SYMBOL; -import static tools.refinery.store.reasoning.ReasoningAdapter.EXISTS_SYMBOL; -import static tools.refinery.store.reasoning.literal.PartialLiterals.may; -import static tools.refinery.store.reasoning.literal.PartialLiterals.must; - public class ScopePropagatorBuilderImpl extends AbstractModelAdapterBuilder<ScopePropagatorStoreAdapter> implements ScopePropagatorBuilder { private Symbol<CardinalityInterval> countSymbol = MultiObjectTranslator.COUNT_STORAGE; @@ -66,16 +60,8 @@ public class ScopePropagatorBuilderImpl extends AbstractModelAdapterBuilder<Scop @Override protected void doConfigure(ModelStoreBuilder storeBuilder) { - var multiQuery = Query.of("MULTI", (builder, instance) -> builder - .clause( - may(EXISTS_SYMBOL.call(instance)), - not(must(EXISTS_SYMBOL.call(instance))) - ) - .clause( - may(EXISTS_SYMBOL.call(instance)), - not(must(EQUALS_SYMBOL.call(instance, instance))) - ) - ); + var multiQuery = Query.of("MULTI", (builder, instance) -> builder.clause( + new MultiView(countSymbol).call(instance))); typeScopePropagatorFactories = new ArrayList<>(scopes.size()); for (var entry : scopes.entrySet()) { var type = entry.getKey(); diff --git a/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/MPSolverTest.java b/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/MPSolverTest.java index c9745d22..95c4ac68 100644 --- a/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/MPSolverTest.java +++ b/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/MPSolverTest.java @@ -49,4 +49,35 @@ class MPSolverTest { assertThat(solver.solve(), is(MPSolver.ResultStatus.OPTIMAL)); assertThat(objective.value(), closeTo(1, 0.01)); } + + @Test + void unboundedIsInfeasibleTest() { + var solver = MPSolver.createSolver("GLOP"); + var x = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "x"); + var objective = solver.objective(); + objective.setCoefficient(x, 1); + + objective.setMinimization(); + assertThat(solver.solve(), is(MPSolver.ResultStatus.OPTIMAL)); + assertThat(objective.value(), closeTo(0, 0.01)); + + objective.setMaximization(); + assertThat(solver.solve(), is(MPSolver.ResultStatus.INFEASIBLE)); + } + + @Test + void constantTest() { + var solver = MPSolver.createSolver("GLOP"); + var x = solver.makeNumVar(1, 1, "x"); + var objective = solver.objective(); + objective.setCoefficient(x, 1); + + objective.setMinimization(); + assertThat(solver.solve(), is(MPSolver.ResultStatus.OPTIMAL)); + assertThat(objective.value(), closeTo(1, 0.01)); + + objective.setMaximization(); + assertThat(solver.solve(), is(MPSolver.ResultStatus.OPTIMAL)); + assertThat(objective.value(), closeTo(1, 0.01)); + } } diff --git a/subprojects/store/src/main/java/tools/refinery/store/representation/cardinality/NonEmptyCardinalityInterval.java b/subprojects/store/src/main/java/tools/refinery/store/representation/cardinality/NonEmptyCardinalityInterval.java index 2e7780da..bfaeea25 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/representation/cardinality/NonEmptyCardinalityInterval.java +++ b/subprojects/store/src/main/java/tools/refinery/store/representation/cardinality/NonEmptyCardinalityInterval.java @@ -83,7 +83,10 @@ public record NonEmptyCardinalityInterval(int lowerBound, UpperCardinality upper @Override public String toString() { - var closeBracket = upperBound instanceof UnboundedUpperCardinality ? ")" : "]"; - return "[%d..%s%s".formatted(lowerBound, upperBound, closeBracket); + if (upperBound instanceof FiniteUpperCardinality finiteUpperCardinality && + finiteUpperCardinality.finiteUpperBound() == lowerBound) { + return "[%d]".formatted(lowerBound); + } + return "[%d..%s]".formatted(lowerBound, upperBound); } } -- cgit v1.2.3-54-g00ecf From 5337f46fbeacd8e188605eaf0af62b651c5f8517 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Sun, 3 Sep 2023 23:43:41 +0200 Subject: build: add Dockerfile --- .../refinery/gradle/java-application.gradle.kts | 2 +- docker/Dockerfile | 46 ++++++++++++++++++++++ docker/build.sh | 38 ++++++++++++++++++ .../refinery/language/web/ServerLauncher.java | 5 ++- 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 docker/Dockerfile create mode 100755 docker/build.sh (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/buildSrc/src/main/kotlin/tools/refinery/gradle/java-application.gradle.kts b/buildSrc/src/main/kotlin/tools/refinery/gradle/java-application.gradle.kts index 0924311b..6c80c971 100644 --- a/buildSrc/src/main/kotlin/tools/refinery/gradle/java-application.gradle.kts +++ b/buildSrc/src/main/kotlin/tools/refinery/gradle/java-application.gradle.kts @@ -25,7 +25,7 @@ dependencies { implementation(libs.slf4j.log4j) } -for (taskName in listOf("distTar", "distZip", "shadowDistTar", "shadowDistZip")) { +for (taskName in listOf("distZip", "shadowDistTar", "shadowDistZip")) { tasks.named(taskName) { enabled = false } diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..ac164cb7 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> +# +# SPDX-License-Identifier: EPL-2.0 + +FROM docker.io/eclipse-temurin:17-jammy AS jlink +# Disable HotSpot C1 compilation, because it causes spurious +# NullPointerException instances to be thrown when running in +# qemu-aarch64-static for image cross-building. +ENV _JAVA_OPTIONS="-XX:CompilationMode=high-only" +# We need to manually add jdk.zipfs to the list of modules output by jdeps to +# enable serving static assets from the refinery-language-web-VERSION.jar, +# because the dependency doesn't get picked up by static analysis. +RUN jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules \ + java.base,java.compiler,java.desktop,java.instrument,java.management,java.naming,java.rmi,java.security.jgss,java.sql,jdk.unsupported,jdk.zipfs \ + --output /jlink + +FROM docker.io/debian:12-slim AS base +# The first layer contains the slimmed down JRE. +COPY --link --from=jlink /jlink /usr/lib/java +ENV JAVA_HOME="/usr/lib/java" PATH="/usr/lib/java/bin:${PATH}" +# Layer with platform-independent dependencies, slow changing. +ADD --link lib /app/lib + +FROM base AS refinery-amd64 +# Layer with platform-dependent dependencies, slow changing. +ADD --link lib_amd64 /app/lib +# Layer with platform-dependent startup script containing references to all +# dependency version. +ADD --link app_amd64_bin /app/bin + +FROM base AS refinery-arm64 +# Layer with platform-dependent dependencies, slow changing. +ADD --link lib_arm64 /app/lib +# Layer with platform-dependent startup script containing references to all +# dependency version. +ADD --link app_arm64_bin /app/bin + +FROM refinery-$TARGETARCH +# Layer with platform-independent application jars. +ADD --link app_lib /app/lib +# Common settings added on top. +ENV LISTEN_ADDRESS=0.0.0.0 LISTEN_PORT=8888 PUBLIC_HOST=localhost PUBLIC_PORT=8888 +EXPOSE 8888 +USER 1000 +WORKDIR /app +ENTRYPOINT /app/bin/refinery-language-web diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 00000000..a8c2aeaa --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> +# +# SPDX-License-Identifier: EPL-2.0 + +set -euo pipefail + +refinery_version="$(grep '^version=' ../gradle.properties | cut -d'=' -f2)" +distribution_name="refinery-language-web-${refinery_version}" +rm -rf "${distribution_name}" dist app_lib app_{amd64,arm64}_bin lib lib_{amd64,arm64} + +tar -xf "../subprojects/language-web/build/distributions/${distribution_name}.tar" +mv "${distribution_name}" dist +mkdir -p app_lib app_{amd64,arm64}_bin lib lib_{amd64,arm64} + +# Move architecture-specific jars to their repsective directories. +mv dist/lib/ortools-linux-x86-64-*.jar lib_amd64 +mv dist/lib/ortools-linux-aarch64-*.jar lib_arm64 +rm dist/lib/ortools-{darwin,win32}-*.jar +# Move the applications jars for the dependencies into a separate Docker layer +# to enable faster updates. +mv dist/lib/refinery-* app_lib +mv dist/lib/* lib +# Omit references to jars not present for the current architecture from the +# startup scripts. +sed 's/:\$APP_HOME\/lib\/ortools-\(darwin\|win32\|linux-aarch64\)[^:]\+\.jar//g' dist/bin/refinery-language-web > app_amd64_bin/refinery-language-web +sed 's/:\$APP_HOME\/lib\/ortools-\(darwin\|win32\|linux-x86-64\)[^:]\+\.jar//g' dist/bin/refinery-language-web > app_arm64_bin/refinery-language-web +chmod a+x app_{amd64,arm64}_bin/refinery-language-web +rm -rf dist + +docker buildx build . \ + --platform linux/amd64,linux/arm64 \ + --output "type=image,name=ghcr.io/graphs4value/refinery:${refinery_version},push=true,annotation-index.org.opencontainers.image.source=https://github.com/graphs4value/refinery,annotation-index.org.opencontainers.image.description=Refinery: an efficient graph solver for generating well-formed models,annotation-index.org.opencontainers.image.licenses=EPL-2.0" \ + --label 'org.opencontainers.image.source=https://github.com/graphs4value/refinery' \ + --label 'org.opencontainers.image.description=Refinery: an efficient graph solver for generating well-formed models' \ + --label 'org.opencontainers.image.licenses=EPL-2.0' \ + --push diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java index ad19e77d..3e1d811b 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java @@ -115,7 +115,10 @@ public class ServerLauncher { // If the app is packaged in the jar, serve it. URI webRootUri; try { - webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/")); + webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString() + .replaceFirst("/index.html$", "/") + // Enable running without warnings from a jar. + .replaceFirst("^jar:file:", "jar:file://")); } catch (URISyntaxException e) { throw new IllegalStateException("Jar has invalid base resource URI", e); } -- cgit v1.2.3-54-g00ecf From 0e7c55f7ab8d496e81a3dbd53f14e0c46cb27fa6 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Mon, 4 Sep 2023 18:08:59 +0200 Subject: refactor: server environemntal variables * Prefix each variable with REFINERY_ * If not public host is specified, allow all origings and compute the WebSocket address on the client from the origin. --- docker/Dockerfile | 2 +- .../frontend/config/detectDevModeOptions.ts | 4 ++-- subprojects/frontend/src/xtext/BackendConfig.ts | 2 +- .../frontend/src/xtext/XtextWebSocketClient.ts | 5 ++++- subprojects/language-web/build.gradle.kts | 12 ++++++++++- .../refinery/language/web/ServerLauncher.java | 24 ++++++++++------------ .../language/web/config/BackendConfigServlet.java | 3 --- 7 files changed, 30 insertions(+), 22 deletions(-) (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/docker/Dockerfile b/docker/Dockerfile index ac164cb7..1485e95b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -39,7 +39,7 @@ FROM refinery-$TARGETARCH # Layer with platform-independent application jars. ADD --link app_lib /app/lib # Common settings added on top. -ENV LISTEN_ADDRESS=0.0.0.0 LISTEN_PORT=8888 PUBLIC_HOST=localhost PUBLIC_PORT=8888 +ENV REFINERY_LISTEN_HOST=0.0.0.0 REFINERY_LISTEN_PORT=8888 EXPOSE 8888 USER 1000 WORKDIR /app diff --git a/subprojects/frontend/config/detectDevModeOptions.ts b/subprojects/frontend/config/detectDevModeOptions.ts index 665204dc..6052e047 100644 --- a/subprojects/frontend/config/detectDevModeOptions.ts +++ b/subprojects/frontend/config/detectDevModeOptions.ts @@ -30,8 +30,8 @@ function detectListenOptions( fallbackHost: string, fallbackPort: number, ): ListenOptions { - const host = process.env[`${name}_HOST`] ?? fallbackHost; - const rawPort = process.env[`${name}_PORT`]; + const host = process.env[`REFINERY_${name}_HOST`] ?? fallbackHost; + const rawPort = process.env[`REFINERY_${name}_PORT`]; const port = rawPort === undefined ? fallbackPort : parseInt(rawPort, 10); const secure = port === 443; return { host, port, secure }; diff --git a/subprojects/frontend/src/xtext/BackendConfig.ts b/subprojects/frontend/src/xtext/BackendConfig.ts index 4c7eac5f..e7043bd5 100644 --- a/subprojects/frontend/src/xtext/BackendConfig.ts +++ b/subprojects/frontend/src/xtext/BackendConfig.ts @@ -11,7 +11,7 @@ import { z } from 'zod'; export const ENDPOINT = 'config.json'; const BackendConfig = z.object({ - webSocketURL: z.string().url(), + webSocketURL: z.string().url().optional(), }); type BackendConfig = z.infer<typeof BackendConfig>; diff --git a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts index 6bb7eec8..963c1d4c 100644 --- a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts +++ b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts @@ -282,7 +282,10 @@ export default class XtextWebSocketClient { log.debug('Creating WebSocket'); (async () => { - const { webSocketURL } = await fetchBackendConfig(); + let { webSocketURL } = await fetchBackendConfig(); + if (webSocketURL === undefined) { + webSocketURL = `${window.origin.replace(/^http/, 'ws')}/xtext-service`; + } this.openWebSocketWithURL(webSocketURL); })().catch((error) => { log.error('Error while initializing connection', error); diff --git a/subprojects/language-web/build.gradle.kts b/subprojects/language-web/build.gradle.kts index 9f772d41..88dccdf3 100644 --- a/subprojects/language-web/build.gradle.kts +++ b/subprojects/language-web/build.gradle.kts @@ -64,8 +64,18 @@ tasks { classpath(mainRuntimeClasspath) mainClass.set(application.mainClass) standardInput = System.`in` - environment("BASE_RESOURCE", webapp.singleFile) + environment("REFINERY_BASE_RESOURCE", webapp.singleFile) group = "run" description = "Start a Jetty web server serving the Xtext API and assets." } + + register<JavaExec>("serveBackendOnly") { + val mainRuntimeClasspath = sourceSets.main.map { it.runtimeClasspath } + dependsOn(mainRuntimeClasspath) + classpath(mainRuntimeClasspath) + mainClass.set(application.mainClass) + standardInput = System.`in` + group = "run" + description = "Start a Jetty web server serving the Xtext API without assets." + } } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java index 3e1d811b..d633b3fc 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java @@ -33,7 +33,7 @@ import java.util.EnumSet; import java.util.Set; public class ServerLauncher { - public static final String DEFAULT_LISTEN_ADDRESS = "localhost"; + public static final String DEFAULT_LISTEN_HOST = "localhost"; public static final int DEFAULT_LISTEN_PORT = 1312; @@ -105,7 +105,7 @@ public class ServerLauncher { private Resource getBaseResource() { var factory = ResourceFactory.of(server); - var baseResourceOverride = System.getenv("BASE_RESOURCE"); + var baseResourceOverride = System.getenv("REFINERY_BASE_RESOURCE"); if (baseResourceOverride != null) { // If a user override is provided, use it. return factory.newResource(baseResourceOverride); @@ -155,15 +155,15 @@ public class ServerLauncher { } private static String getListenAddress() { - var listenAddress = System.getenv("LISTEN_ADDRESS"); + var listenAddress = System.getenv("REFINERY_LISTEN_HOST"); if (listenAddress == null) { - return DEFAULT_LISTEN_ADDRESS; + return DEFAULT_LISTEN_HOST; } return listenAddress; } private static int getListenPort() { - var portStr = System.getenv("LISTEN_PORT"); + var portStr = System.getenv("REFINERY_LISTEN_PORT"); if (portStr != null) { return Integer.parseInt(portStr); } @@ -177,7 +177,7 @@ public class ServerLauncher { } private static String getPublicHost() { - var publicHost = System.getenv("PUBLIC_HOST"); + var publicHost = System.getenv("REFINERY_PUBLIC_HOST"); if (publicHost != null) { return publicHost.toLowerCase(); } @@ -185,7 +185,7 @@ public class ServerLauncher { } private static int getPublicPort() { - var portStr = System.getenv("PUBLIC_PORT"); + var portStr = System.getenv("REFINERY_PUBLIC_PORT"); if (portStr != null) { return Integer.parseInt(portStr); } @@ -193,7 +193,7 @@ public class ServerLauncher { } private static String[] getAllowedOrigins() { - var allowedOrigins = System.getenv("ALLOWED_ORIGINS"); + var allowedOrigins = System.getenv("REFINERY_ALLOWED_ORIGINS"); if (allowedOrigins != null) { return allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR); } @@ -222,12 +222,10 @@ public class ServerLauncher { int port; var publicHost = getPublicHost(); if (publicHost == null) { - host = getListenAddress(); - port = getListenPort(); - } else { - host = publicHost; - port = getPublicPort(); + return null; } + host = publicHost; + port = getPublicPort(); var scheme = port == HTTPS_DEFAULT_PORT ? "wss" : "ws"; return String.format("%s://%s:%d/xtext-service", scheme, host, port); } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java index a2f04e34..7d0a5122 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java @@ -25,9 +25,6 @@ public class BackendConfigServlet extends HttpServlet { public void init(ServletConfig config) throws ServletException { super.init(config); var webSocketUrl = config.getInitParameter(WEBSOCKET_URL_INIT_PARAM); - if (webSocketUrl == null) { - throw new IllegalArgumentException("Init parameter " + WEBSOCKET_URL_INIT_PARAM + " is mandatory"); - } var backendConfig = new BackendConfig(webSocketUrl); var gson = new Gson(); serializedConfig = gson.toJson(backendConfig); -- cgit v1.2.3-54-g00ecf From 72a3f0f50560d70f2f2ff3e1ab6dbb92f80a5f0f Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Tue, 5 Sep 2023 02:20:57 +0200 Subject: feat(web): control server settings with env vars --- .../refinery/language/web/ProblemWebModule.java | 6 ++ .../refinery/language/web/ServerLauncher.java | 4 +- .../language/web/semantics/SemanticsService.java | 31 +++++- .../server/ThreadPoolExecutorServiceProvider.java | 110 +++++++++++++++++++++ .../web/tests/ProblemWebInjectorProvider.java | 1 + 5 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java index b0197c01..6a6e0107 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java @@ -9,11 +9,13 @@ */ package tools.refinery.language.web; +import org.eclipse.xtext.ide.ExecutorServiceProvider; import org.eclipse.xtext.web.server.XtextServiceDispatcher; import org.eclipse.xtext.web.server.model.IWebDocumentProvider; import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; import org.eclipse.xtext.web.server.occurrences.OccurrencesService; import tools.refinery.language.web.occurrences.ProblemOccurrencesService; +import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider; import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher; import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider; @@ -37,4 +39,8 @@ public class ProblemWebModule extends AbstractProblemWebModule { public Class<? extends OccurrencesService> bindOccurrencesService() { return ProblemOccurrencesService.class; } + + public Class<? extends ExecutorServiceProvider> bindExecutorServiceProvider() { + return ThreadPoolExecutorServiceProvider.class; + } } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java index d633b3fc..155efc6f 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java @@ -165,7 +165,7 @@ public class ServerLauncher { private static int getListenPort() { var portStr = System.getenv("REFINERY_LISTEN_PORT"); if (portStr != null) { - return Integer.parseInt(portStr); + return Integer.parseUnsignedInt(portStr); } return DEFAULT_LISTEN_PORT; } @@ -187,7 +187,7 @@ public class ServerLauncher { private static int getPublicPort() { var portStr = System.getenv("REFINERY_PUBLIC_PORT"); if (portStr != null) { - return Integer.parseInt(portStr); + return Integer.parseUnsignedInt(portStr); } return DEFAULT_PUBLIC_PORT; } 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 ba55dc77..26924f0a 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 @@ -22,10 +22,14 @@ import tools.refinery.language.model.problem.Problem; import tools.refinery.language.web.xtext.server.push.PushWebDocument; import java.util.List; +import java.util.Optional; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; @Singleton public class SemanticsService extends AbstractCachedService<SemanticsResult> { + public static final String SEMANTICS_EXECUTOR = "semantics"; + private static final Logger LOG = LoggerFactory.getLogger(SemanticsService.class); @Inject @@ -39,9 +43,24 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { private ExecutorService executorService; + private final long timeoutMs; + + private final long warmupTimeoutMs; + + private final AtomicBoolean warmedUp = new AtomicBoolean(false); + + public SemanticsService() { + timeoutMs = getTimeout("REFINERY_SEMANTICS_TIMEOUT_MS").orElse(1000L); + warmupTimeoutMs = getTimeout("REFINERY_SEMANTICS_WARMUP_TIMEOUT_MS").orElse(timeoutMs * 2); + } + + private static Optional<Long> getTimeout(String name) { + return Optional.ofNullable(System.getenv(name)).map(Long::parseUnsignedLong); + } + @Inject public void setExecutorServiceProvider(ExecutorServiceProvider provider) { - executorService = provider.get(this.getClass().getName()); + executorService = provider.get(SEMANTICS_EXECUTOR); } @Override @@ -60,9 +79,14 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { var worker = workerProvider.get(); worker.setProblem(problem, cancelIndicator); var future = executorService.submit(worker); + boolean warmedUpCurrently = warmedUp.get(); + long timeout = warmedUpCurrently ? timeoutMs : warmupTimeoutMs; SemanticsResult result = null; try { - result = future.get(2, TimeUnit.SECONDS); + result = future.get(timeout, TimeUnit.MILLISECONDS); + if (!warmedUpCurrently) { + warmedUp.set(true); + } } catch (InterruptedException e) { future.cancel(true); LOG.error("Semantics service interrupted", e); @@ -80,6 +104,9 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { return new SemanticsInternalErrorResult(message); } catch (TimeoutException e) { future.cancel(true); + if (!warmedUpCurrently) { + warmedUp.set(true); + } LOG.trace("Semantics service timeout", e); return new SemanticsInternalErrorResult("Partial interpretation timed out"); } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java new file mode 100644 index 00000000..ba26ff58 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.xtext.server; + +import com.google.inject.Singleton; +import org.eclipse.xtext.ide.ExecutorServiceProvider; +import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; +import org.jetbrains.annotations.NotNull; +import tools.refinery.language.web.semantics.SemanticsService; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +@Singleton +public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { + private static final String DOCUMENT_LOCK_EXECUTOR; + private static final AtomicInteger POOL_ID = new AtomicInteger(1); + + private final int executorThreadCount; + private final int lockExecutorThreadCount; + private final int semanticsExecutorThreadCount; + + static { + var lookup = MethodHandles.lookup(); + MethodHandle getter; + try { + var privateLookup = MethodHandles.privateLookupIn(XtextWebDocumentAccess.class, lookup); + getter = privateLookup.findStaticGetter(XtextWebDocumentAccess.class, "DOCUMENT_LOCK_EXECUTOR", + String.class); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new IllegalStateException("Failed to find getter", e); + } + try { + DOCUMENT_LOCK_EXECUTOR = (String) getter.invokeExact(); + } catch (Error e) { + // Rethrow JVM errors. + throw e; + } catch (Throwable e) { + throw new IllegalStateException("Failed to get DOCUMENT_LOCK_EXECUTOR", e); + } + } + + public ThreadPoolExecutorServiceProvider() { + executorThreadCount = getCount("REFINERY_XTEXT_THREAD_COUNT").orElse(0); + lockExecutorThreadCount = getCount("REFINERY_XTEXT_LOCKING_THREAD_COUNT").orElse(executorThreadCount); + semanticsExecutorThreadCount = getCount("REFINERY_XTEXT_SEMANTICS_THREAD_COUNT").orElse(executorThreadCount); + } + + private static Optional<Integer> getCount(String name) { + return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt); + } + + @Override + protected ExecutorService createInstance(String key) { + String name = "xtext-" + POOL_ID.getAndIncrement(); + if (key != null) { + name = name + key + "-"; + } + var threadFactory = new Factory(name, 5); + int size = getSize(key); + if (size == 0) { + return Executors.newCachedThreadPool(threadFactory); + } + return Executors.newFixedThreadPool(size, threadFactory); + } + + private int getSize(String key) { + if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) { + return semanticsExecutorThreadCount; + } else if (DOCUMENT_LOCK_EXECUTOR.equals(key)) { + return lockExecutorThreadCount; + } else { + return executorThreadCount; + } + } + + private static class Factory implements ThreadFactory { + // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}. + @SuppressWarnings("squid:S3014") + private final ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); + private final AtomicInteger threadId = new AtomicInteger(1); + private final String namePrefix; + private final int priority; + + public Factory(String name, int priority) { + namePrefix = name + "-thread-"; + this.priority = priority; + } + + @Override + public Thread newThread(@NotNull Runnable runnable) { + var thread = new Thread(threadGroup, runnable, namePrefix + threadId.getAndIncrement()); + if (thread.isDaemon()) { + thread.setDaemon(false); + } + if (thread.getPriority() != priority) { + thread.setPriority(priority); + } + return thread; + } + } +} diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java index 4a5eed95..e9d889c4 100644 --- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java @@ -34,6 +34,7 @@ public class ProblemWebInjectorProvider extends ProblemInjectorProvider { // the tasks in the service and the {@link // org.eclipse.xtext.testing.extensions.InjectionExtension}. return new ProblemWebModule() { + @Override @SuppressWarnings("unused") public Class<? extends ExecutorServiceProvider> bindExecutorServiceProvider() { return AwaitTerminationExecutorServiceProvider.class; -- cgit v1.2.3-54-g00ecf From 37bcdc4c80f8c5d15ba888aace70f413094910ed Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Mon, 11 Sep 2023 01:27:30 +0200 Subject: fix: build failures after integrating generation --- .../language/web/semantics/SemanticsWorker.java | 10 +- .../store/reasoning/scope/MultiObjectTest.java | 44 ++- .../translator/multiobject/CandidateCountTest.java | 321 --------------------- 3 files changed, 24 insertions(+), 351 deletions(-) delete mode 100644 subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/multiobject/CandidateCountTest.java (limited to 'subprojects/language-web/src/main/java/tools') 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 8470bb99..c745d86e 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 @@ -22,6 +22,7 @@ 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; +import tools.refinery.store.dse.propagation.PropagationAdapter; import tools.refinery.store.map.Cursor; import tools.refinery.store.model.Model; import tools.refinery.store.model.ModelStore; @@ -29,9 +30,7 @@ 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.refinement.RefinementResult; import tools.refinery.store.reasoning.representation.PartialRelation; -import tools.refinery.store.reasoning.scope.ScopePropagatorAdapter; import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; import tools.refinery.store.tuple.Tuple; @@ -77,9 +76,9 @@ class SemanticsWorker implements Callable<SemanticsResult> { var builder = ModelStore.builder() .with(ViatraModelQueryAdapter.builder() .cancellationToken(cancellationToken)) + .with(PropagationAdapter.builder()) .with(ReasoningAdapter.builder() - .requiredInterpretations(Concreteness.PARTIAL)) - .with(ScopePropagatorAdapter.builder()); + .requiredInterpretations(Concreteness.PARTIAL)); cancellationToken.checkCancelled(); try { var modelSeed = initializer.createModel(problem, builder); @@ -94,9 +93,6 @@ class SemanticsWorker implements Callable<SemanticsResult> { cancellationToken.checkCancelled(); var cancellableModelSeed = CancellableSeed.wrap(cancellationToken, modelSeed); var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(cancellableModelSeed); - if (model.getAdapter(ScopePropagatorAdapter.class).propagate() == RefinementResult.REJECTED) { - return new SemanticsInternalErrorResult("Scopes are unsatisfiable"); - } cancellationToken.checkCancelled(); var partialInterpretation = getPartialInterpretation(initializer, model); diff --git a/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/MultiObjectTest.java b/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/MultiObjectTest.java index 5fc70ae1..0132b3f9 100644 --- a/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/MultiObjectTest.java +++ b/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/MultiObjectTest.java @@ -27,11 +27,13 @@ import tools.refinery.store.tuple.Tuple; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; class MultiObjectTest { private static final PartialRelation person = new PartialRelation("Person", 1); private ModelStore store; + private ReasoningStoreAdapter reasoningStoreAdapter; private Model model; private Interpretation<CardinalityInterval> countStorage; @@ -47,6 +49,7 @@ class MultiObjectTest { .with(new ScopePropagator() .scope(person, CardinalityIntervals.between(5, 15))) .build(); + reasoningStoreAdapter = store.getAdapter(ReasoningStoreAdapter.class); model = null; countStorage = null; } @@ -59,7 +62,6 @@ class MultiObjectTest { .put(Tuple.of(0), CardinalityIntervals.SET)) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) .build()); - assertThat(propagate(), is(PropagationResult.PROPAGATED)); assertThat(countStorage.get(Tuple.of(0)), is(CardinalityIntervals.between(2, 12))); } @@ -71,19 +73,18 @@ class MultiObjectTest { .put(Tuple.of(0), CardinalityIntervals.between(5, 20))) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) .build()); - assertThat(propagate(), is(PropagationResult.PROPAGATED)); assertThat(countStorage.get(Tuple.of(0)), is(CardinalityIntervals.between(5, 12))); } @Test void oneMultiObjectUnsatisfiableUpperTest() { - createModel(ModelSeed.builder(21) + var seed = ModelSeed.builder(21) .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder .reducedValue(CardinalityIntervals.ONE) .put(Tuple.of(0), CardinalityIntervals.SET)) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .build()); - assertThat(propagate(), is(PropagationResult.REJECTED)); + .build(); + assertThrows(IllegalArgumentException.class, () -> reasoningStoreAdapter.createInitialModel(seed)); } @Test @@ -97,33 +98,33 @@ class MultiObjectTest { @Test void noMultiObjectUnsatisfiableTest() { - createModel(ModelSeed.builder(2) + var seed = ModelSeed.builder(2) .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder.reducedValue(CardinalityIntervals.ONE)) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .build()); - assertThat(propagate(), is(PropagationResult.REJECTED)); + .build(); + assertThrows(IllegalArgumentException.class, () -> reasoningStoreAdapter.createInitialModel(seed)); } @Test void oneMultiObjectExistingBoundUnsatisfiableLowerTest() { - createModel(ModelSeed.builder(4) + var seed = ModelSeed.builder(4) .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder .reducedValue(CardinalityIntervals.ONE) .put(Tuple.of(0), CardinalityIntervals.atLeast(20))) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .build()); - assertThat(propagate(), is(PropagationResult.REJECTED)); + .build(); + assertThrows(IllegalArgumentException.class, () -> reasoningStoreAdapter.createInitialModel(seed)); } @Test void oneMultiObjectExistingBoundUnsatisfiableUpperTest() { - createModel(ModelSeed.builder(4) + var seed = ModelSeed.builder(4) .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder .reducedValue(CardinalityIntervals.ONE) .put(Tuple.of(0), CardinalityIntervals.atMost(1))) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .build()); - assertThat(propagate(), is(PropagationResult.REJECTED)); + .build(); + assertThrows(IllegalArgumentException.class, () -> reasoningStoreAdapter.createInitialModel(seed)); } @Test @@ -135,7 +136,6 @@ class MultiObjectTest { .put(Tuple.of(1), CardinalityIntervals.SET)) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) .build()); - assertThat(propagate(), is(PropagationResult.PROPAGATED)); assertThat(countStorage.get(Tuple.of(0)), is(CardinalityIntervals.atMost(12))); assertThat(countStorage.get(Tuple.of(1)), is(CardinalityIntervals.atMost(12))); } @@ -149,33 +149,32 @@ class MultiObjectTest { .put(Tuple.of(1), CardinalityIntervals.atMost(11))) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) .build()); - assertThat(propagate(), is(PropagationResult.PROPAGATED)); assertThat(countStorage.get(Tuple.of(0)), is(CardinalityIntervals.between(7, 12))); assertThat(countStorage.get(Tuple.of(1)), is(CardinalityIntervals.atMost(5))); } @Test void twoMultiObjectsExistingBoundUnsatisfiableUpperTest() { - createModel(ModelSeed.builder(5) + var seed = ModelSeed.builder(5) .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder .reducedValue(CardinalityIntervals.ONE) .put(Tuple.of(0), CardinalityIntervals.between(7, 20)) .put(Tuple.of(1), CardinalityIntervals.exactly(11))) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .build()); - assertThat(propagate(), is(PropagationResult.REJECTED)); + .build(); + assertThrows(IllegalArgumentException.class, () -> reasoningStoreAdapter.createInitialModel(seed)); } @Test void twoMultiObjectsExistingBoundUnsatisfiableLowerTest() { - createModel(ModelSeed.builder(3) + var seed = ModelSeed.builder(3) .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder .reducedValue(CardinalityIntervals.ONE) .put(Tuple.of(0), CardinalityIntervals.LONE) .put(Tuple.of(1), CardinalityIntervals.atMost(2))) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .build()); - assertThat(propagate(), is(PropagationResult.REJECTED)); + .build(); + assertThrows(IllegalArgumentException.class, () -> reasoningStoreAdapter.createInitialModel(seed)); } @Test @@ -187,7 +186,6 @@ class MultiObjectTest { .put(Tuple.of(1), CardinalityIntervals.SET)) .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) .build()); - assertThat(propagate(), is(PropagationResult.PROPAGATED)); assertThat(countStorage.get(Tuple.of(0)), is(CardinalityIntervals.LONE)); assertThat(countStorage.get(Tuple.of(1)), is(CardinalityIntervals.between(1, 12))); countStorage.put(Tuple.of(0), CardinalityIntervals.ONE); diff --git a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/multiobject/CandidateCountTest.java b/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/multiobject/CandidateCountTest.java deleted file mode 100644 index 28391ec7..00000000 --- a/subprojects/store-reasoning/src/test/java/tools/refinery/store/reasoning/translator/multiobject/CandidateCountTest.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.store.reasoning.translator.multiobject; - -import org.junit.jupiter.api.Test; -import tools.refinery.store.model.ModelStore; -import tools.refinery.store.query.ModelQueryAdapter; -import tools.refinery.store.query.dnf.Query; -import tools.refinery.store.query.resultset.ResultSet; -import tools.refinery.store.query.term.Variable; -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.CountCandidateLowerBoundLiteral; -import tools.refinery.store.reasoning.literal.CountCandidateUpperBoundLiteral; -import tools.refinery.store.reasoning.representation.PartialRelation; -import tools.refinery.store.reasoning.seed.ModelSeed; -import tools.refinery.store.reasoning.translator.PartialRelationTranslator; -import tools.refinery.store.representation.Symbol; -import tools.refinery.store.representation.TruthValue; -import tools.refinery.store.representation.cardinality.CardinalityIntervals; -import tools.refinery.store.tuple.Tuple; - -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static tools.refinery.store.query.literal.Literals.not; -import static tools.refinery.store.reasoning.literal.PartialLiterals.must; - -class CandidateCountTest { - private static final PartialRelation person = new PartialRelation("Person", 1); - private static final PartialRelation friend = new PartialRelation("friend", 2); - - @Test - void lowerBoundZeroTest() { - var query = Query.of("LowerBound", Integer.class, (builder, p1, p2, output) -> builder.clause( - must(person.call(p1)), - must(person.call(p2)), - new CountCandidateLowerBoundLiteral(output, friend, List.of(p1, p2)) - )); - - var modelSeed = ModelSeed.builder(2) - .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder - .put(Tuple.of(0), CardinalityIntervals.atLeast(3)) - .put(Tuple.of(1), CardinalityIntervals.atMost(7))) - .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .seed(friend, builder -> builder - .put(Tuple.of(0, 1), TruthValue.TRUE) - .put(Tuple.of(1, 0), TruthValue.UNKNOWN) - .put(Tuple.of(1, 1), TruthValue.ERROR)) - .build(); - - var resultSet = getResultSet(query, modelSeed); - assertThat(resultSet.get(Tuple.of(0, 0)), is(0)); - assertThat(resultSet.get(Tuple.of(0, 1)), is(1)); - assertThat(resultSet.get(Tuple.of(1, 0)), is(0)); - assertThat(resultSet.get(Tuple.of(1, 1)), is(1)); - } - - @Test - void upperBoundZeroTest() { - var query = Query.of("UpperBound", Integer.class, (builder, p1, p2, output) -> builder.clause( - must(person.call(p1)), - must(person.call(p2)), - new CountCandidateUpperBoundLiteral(output, friend, List.of(p1, p2)) - )); - - var modelSeed = ModelSeed.builder(2) - .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder - .put(Tuple.of(0), CardinalityIntervals.atLeast(3)) - .put(Tuple.of(1), CardinalityIntervals.atMost(7))) - .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .seed(friend, builder -> builder - .put(Tuple.of(0, 1), TruthValue.TRUE) - .put(Tuple.of(1, 0), TruthValue.UNKNOWN) - .put(Tuple.of(1, 1), TruthValue.ERROR)) - .build(); - - var resultSet = getResultSet(query, modelSeed); - assertThat(resultSet.get(Tuple.of(0, 0)), is(0)); - assertThat(resultSet.get(Tuple.of(0, 1)), is(1)); - assertThat(resultSet.get(Tuple.of(1, 0)), is(0)); - assertThat(resultSet.get(Tuple.of(1, 1)), is(1)); - } - - @Test - void lowerBoundOneTest() { - var query = Query.of("LowerBound", Integer.class, (builder, p1, output) -> builder.clause( - must(person.call(p1)), - new CountCandidateLowerBoundLiteral(output, friend, List.of(p1, Variable.of())) - )); - - var modelSeed = ModelSeed.builder(4) - .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder - .reducedValue(CardinalityIntervals.ONE) - .put(Tuple.of(1), CardinalityIntervals.atLeast(3)) - .put(Tuple.of(2), CardinalityIntervals.atMost(7))) - .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .seed(friend, builder -> builder - .put(Tuple.of(0, 1), TruthValue.TRUE) - .put(Tuple.of(0, 2), TruthValue.TRUE) - .put(Tuple.of(0, 3), TruthValue.TRUE) - .put(Tuple.of(1, 0), TruthValue.TRUE) - .put(Tuple.of(1, 2), TruthValue.UNKNOWN) - .put(Tuple.of(1, 3), TruthValue.UNKNOWN) - .put(Tuple.of(2, 0), TruthValue.TRUE) - .put(Tuple.of(2, 1), TruthValue.ERROR)) - .build(); - - var resultSet = getResultSet(query, modelSeed); - assertThat(resultSet.get(Tuple.of(0)), is(2)); - assertThat(resultSet.get(Tuple.of(1)), is(1)); - assertThat(resultSet.get(Tuple.of(2)), is(2)); - assertThat(resultSet.get(Tuple.of(3)), is(0)); - } - - @Test - void upperBoundOneTest() { - var query = Query.of("UpperBound", Integer.class, (builder, p1, output) -> builder.clause( - must(person.call(p1)), - new CountCandidateUpperBoundLiteral(output, friend, List.of(p1, Variable.of())) - )); - - var modelSeed = ModelSeed.builder(4) - .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder - .reducedValue(CardinalityIntervals.ONE) - .put(Tuple.of(1), CardinalityIntervals.atLeast(3)) - .put(Tuple.of(2), CardinalityIntervals.atMost(7))) - .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .seed(friend, builder -> builder - .put(Tuple.of(0, 1), TruthValue.TRUE) - .put(Tuple.of(0, 2), TruthValue.TRUE) - .put(Tuple.of(0, 3), TruthValue.TRUE) - .put(Tuple.of(1, 0), TruthValue.TRUE) - .put(Tuple.of(1, 2), TruthValue.UNKNOWN) - .put(Tuple.of(1, 3), TruthValue.UNKNOWN) - .put(Tuple.of(2, 0), TruthValue.TRUE) - .put(Tuple.of(2, 1), TruthValue.ERROR)) - .build(); - - var resultSet = getResultSet(query, modelSeed); - assertThat(resultSet.get(Tuple.of(0)), is(2)); - assertThat(resultSet.get(Tuple.of(1)), is(1)); - assertThat(resultSet.get(Tuple.of(2)), is(2)); - assertThat(resultSet.get(Tuple.of(3)), is(0)); - } - - @Test - void lowerBoundTwoTest() { - var subQuery = Query.of("SubQuery", (builder, p1, p2, p3) -> builder.clause( - friend.call(p1, p2), - friend.call(p1, p3), - friend.call(p2, p3) - )); - var query = Query.of("LowerBound", Integer.class, (builder, p1, output) -> builder.clause( - must(person.call(p1)), - new CountCandidateLowerBoundLiteral(output, subQuery.getDnf(), - List.of(p1, Variable.of(), Variable.of())) - )); - - var modelSeed = ModelSeed.builder(4) - .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder - .reducedValue(CardinalityIntervals.ONE) - .put(Tuple.of(0), CardinalityIntervals.between(5, 9)) - .put(Tuple.of(1), CardinalityIntervals.atLeast(3)) - .put(Tuple.of(2), CardinalityIntervals.atMost(7))) - .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .seed(friend, builder -> builder - .put(Tuple.of(0, 1), TruthValue.TRUE) - .put(Tuple.of(0, 2), TruthValue.TRUE) - .put(Tuple.of(0, 3), TruthValue.TRUE) - .put(Tuple.of(1, 0), TruthValue.TRUE) - .put(Tuple.of(1, 2), TruthValue.TRUE) - .put(Tuple.of(1, 3), TruthValue.TRUE) - .put(Tuple.of(2, 0), TruthValue.TRUE) - .put(Tuple.of(2, 1), TruthValue.ERROR)) - .build(); - - var resultSet = getResultSet(query, modelSeed); - assertThat(resultSet.get(Tuple.of(0)), is(1)); - assertThat(resultSet.get(Tuple.of(1)), is(1)); - assertThat(resultSet.get(Tuple.of(2)), is(2)); - assertThat(resultSet.get(Tuple.of(3)), is(0)); - } - - @Test - void upperBoundTwoTest() { - var subQuery = Query.of("SubQuery", (builder, p1, p2, p3) -> builder.clause( - friend.call(p1, p2), - friend.call(p1, p3), - friend.call(p2, p3) - )); - var query = Query.of("UpperBound", Integer.class, (builder, p1, output) -> builder.clause( - must(person.call(p1)), - new CountCandidateUpperBoundLiteral(output, subQuery.getDnf(), - List.of(p1, Variable.of(), Variable.of())) - )); - - var modelSeed = ModelSeed.builder(4) - .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder - .reducedValue(CardinalityIntervals.ONE) - .put(Tuple.of(0), CardinalityIntervals.between(5, 9)) - .put(Tuple.of(1), CardinalityIntervals.atLeast(3)) - .put(Tuple.of(2), CardinalityIntervals.atMost(7))) - .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .seed(friend, builder -> builder - .put(Tuple.of(0, 1), TruthValue.TRUE) - .put(Tuple.of(0, 2), TruthValue.TRUE) - .put(Tuple.of(0, 3), TruthValue.TRUE) - .put(Tuple.of(1, 0), TruthValue.TRUE) - .put(Tuple.of(1, 2), TruthValue.UNKNOWN) - .put(Tuple.of(1, 3), TruthValue.UNKNOWN) - .put(Tuple.of(2, 0), TruthValue.TRUE) - .put(Tuple.of(2, 1), TruthValue.ERROR)) - .build(); - - var resultSet = getResultSet(query, modelSeed); - assertThat(resultSet.get(Tuple.of(0)), is(0)); - assertThat(resultSet.get(Tuple.of(1)), is(0)); - assertThat(resultSet.get(Tuple.of(2)), is(2)); - assertThat(resultSet.get(Tuple.of(3)), is(0)); - } - - @Test - void lowerBoundDiagonalTest() { - var subQuery = Query.of("SubQuery", (builder, p1, p2, p3) -> builder.clause( - friend.call(p1, p2), - friend.call(p1, p3), - not(friend.call(p2, p3)) - )); - var query = Query.of("LowerBound", Integer.class, (builder, p1, output) -> builder.clause(v1 -> List.of( - must(person.call(p1)), - new CountCandidateLowerBoundLiteral(output, subQuery.getDnf(), List.of(p1, v1, v1)) - ))); - - var modelSeed = ModelSeed.builder(4) - .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder - .reducedValue(CardinalityIntervals.ONE) - .put(Tuple.of(0), CardinalityIntervals.between(5, 9)) - .put(Tuple.of(1), CardinalityIntervals.atLeast(3)) - .put(Tuple.of(2), CardinalityIntervals.atMost(7))) - .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .seed(friend, builder -> builder - .put(Tuple.of(0, 1), TruthValue.TRUE) - .put(Tuple.of(0, 2), TruthValue.TRUE) - .put(Tuple.of(0, 3), TruthValue.TRUE) - .put(Tuple.of(1, 0), TruthValue.TRUE) - .put(Tuple.of(1, 2), TruthValue.UNKNOWN) - .put(Tuple.of(1, 3), TruthValue.UNKNOWN) - .put(Tuple.of(2, 0), TruthValue.TRUE) - .put(Tuple.of(2, 1), TruthValue.ERROR)) - .build(); - - var resultSet = getResultSet(query, modelSeed); - assertThat(resultSet.get(Tuple.of(0)), is(2)); - assertThat(resultSet.get(Tuple.of(1)), is(1)); - assertThat(resultSet.get(Tuple.of(2)), is(2)); - assertThat(resultSet.get(Tuple.of(3)), is(0)); - } - - @Test - void upperBoundDiagonalTest() { - var subQuery = Query.of("SubQuery", (builder, p1, p2, p3) -> builder.clause( - friend.call(p1, p2), - friend.call(p1, p3), - not(friend.call(p2, p3)) - )); - var query = Query.of("UpperBound", Integer.class, (builder, p1, output) -> builder - .clause(v1 -> List.of( - must(person.call(p1)), - new CountCandidateUpperBoundLiteral(output, subQuery.getDnf(), List.of(p1, v1, v1)) - ))); - - var modelSeed = ModelSeed.builder(4) - .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder - .reducedValue(CardinalityIntervals.ONE) - .put(Tuple.of(0), CardinalityIntervals.between(5, 9)) - .put(Tuple.of(1), CardinalityIntervals.atLeast(3)) - .put(Tuple.of(2), CardinalityIntervals.atMost(7))) - .seed(person, builder -> builder.reducedValue(TruthValue.TRUE)) - .seed(friend, builder -> builder - .put(Tuple.of(0, 1), TruthValue.TRUE) - .put(Tuple.of(0, 2), TruthValue.TRUE) - .put(Tuple.of(0, 3), TruthValue.TRUE) - .put(Tuple.of(1, 0), TruthValue.TRUE) - .put(Tuple.of(1, 2), TruthValue.UNKNOWN) - .put(Tuple.of(1, 3), TruthValue.UNKNOWN) - .put(Tuple.of(2, 0), TruthValue.TRUE) - .put(Tuple.of(2, 1), TruthValue.ERROR)) - .build(); - - var resultSet = getResultSet(query, modelSeed); - assertThat(resultSet.get(Tuple.of(0)), is(2)); - assertThat(resultSet.get(Tuple.of(1)), is(1)); - assertThat(resultSet.get(Tuple.of(2)), is(2)); - assertThat(resultSet.get(Tuple.of(3)), is(0)); - } - - private static <T> ResultSet<T> getResultSet(Query<T> query, ModelSeed modelSeed) { - var personStorage = Symbol.of("Person", 1, TruthValue.class, TruthValue.FALSE); - var friendStorage = Symbol.of("friend", 2, TruthValue.class, TruthValue.FALSE); - - var store = ModelStore.builder() - .with(ViatraModelQueryAdapter.builder() - .query(query)) - .with(ReasoningAdapter.builder()) - .with(new MultiObjectTranslator()) - .with(PartialRelationTranslator.of(person) - .symbol(personStorage)) - .with(PartialRelationTranslator.of(friend) - .symbol(friendStorage)) - .build(); - - var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); - return model.getAdapter(ModelQueryAdapter.class).getResultSet(query); - } -} -- cgit v1.2.3-54-g00ecf From 4d365b54dad8d066bba2a2b1a05092b4802b9970 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Mon, 11 Sep 2023 19:22:26 +0200 Subject: feat: cancellation token for ModelStore --- .../semantics/model/ModelGenerationTest.java | 2 +- .../language/web/semantics/CancellableSeed.java | 99 ---------------------- .../language/web/semantics/SemanticsWorker.java | 9 +- .../propagation/impl/PropagationAdapterImpl.java | 3 + .../impl/rule/BoundPropagationRule.java | 3 + .../store/dse/strategy/BestFirstExplorer.java | 9 +- .../store/dse/transition/actions/BoundAction.java | 1 + .../DesignSpaceExplorationAdapterImpl.java | 4 + .../query/viatra/ViatraModelQueryBuilder.java | 3 - .../internal/ViatraModelQueryBuilderImpl.java | 10 +-- .../reasoning/scope/BoundScopePropagator.java | 7 ++ .../multiobject/MultiObjectInitializer.java | 16 ++-- .../typehierarchy/TypeHierarchyInitializer.java | 7 +- .../java/tools/refinery/store/model/Model.java | 4 +- .../tools/refinery/store/model/ModelStore.java | 2 + .../refinery/store/model/ModelStoreBuilder.java | 3 + .../refinery/store/model/internal/ModelImpl.java | 16 +++- .../model/internal/ModelStoreBuilderImpl.java | 21 ++++- .../store/model/internal/ModelStoreImpl.java | 15 +++- .../model/internal/VersionedInterpretation.java | 6 +- .../statecoding/StateCodeCalculatorFactory.java | 3 +- .../internal/StateCoderStoreAdapterImpl.java | 2 +- .../AbstractNeighbourhoodCalculator.java | 6 +- .../neighbourhood/LazyNeighbourhoodCalculator.java | 6 +- .../neighbourhood/NeighbourhoodCalculator.java | 8 +- .../refinery/store/util/CancellationToken.java | 13 +++ .../store/statecoding/EquivalenceTest.java | 3 +- .../store/statecoding/StateCoderBuildTest.java | 3 +- 28 files changed, 131 insertions(+), 153 deletions(-) delete mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java create mode 100644 subprojects/store/src/main/java/tools/refinery/store/util/CancellationToken.java (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/ModelGenerationTest.java b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/ModelGenerationTest.java index ecd5d39c..d756099c 100644 --- a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/ModelGenerationTest.java +++ b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/model/ModelGenerationTest.java @@ -209,7 +209,7 @@ class ModelGenerationTest { error choiceHasNoIncoming(Choice c) <-> !target(_, c). - scope node = 50..60, Region = 5..10, Statechart = 1. + scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1. """); assertThat(parsedProblem.errors(), empty()); var problem = parsedProblem.problem(); diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java deleted file mode 100644 index aa14f39d..00000000 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.web.semantics; - -import tools.refinery.store.map.AnyVersionedMap; -import tools.refinery.store.map.Cursor; -import tools.refinery.store.reasoning.representation.PartialSymbol; -import tools.refinery.store.reasoning.seed.ModelSeed; -import tools.refinery.store.reasoning.seed.Seed; -import tools.refinery.store.tuple.Tuple; -import tools.refinery.viatra.runtime.CancellationToken; - -import java.util.Set; - -class CancellableSeed<T> implements Seed<T> { - private final CancellationToken cancellationToken; - private final Seed<T> seed; - - private CancellableSeed(CancellationToken cancellationToken, Seed<T> seed) { - this.cancellationToken = cancellationToken; - this.seed = seed; - } - - @Override - public int arity() { - return seed.arity(); - } - - @Override - public Class<T> valueType() { - return seed.valueType(); - } - - @Override - public T reducedValue() { - return seed.reducedValue(); - } - - @Override - public T get(Tuple key) { - return seed.get(key); - } - - @Override - public Cursor<Tuple, T> getCursor(T defaultValue, int nodeCount) { - return new CancellableCursor<>(cancellationToken, seed.getCursor(defaultValue, nodeCount)); - } - - public static ModelSeed wrap(CancellationToken cancellationToken, ModelSeed modelSeed) { - var builder = ModelSeed.builder(modelSeed.getNodeCount()); - for (var partialSymbol : modelSeed.getSeededSymbols()) { - wrap(cancellationToken, (PartialSymbol<?, ?>) partialSymbol, modelSeed, builder); - } - return builder.build(); - } - - private static <A, C> void wrap(CancellationToken cancellationToken, PartialSymbol<A, C> partialSymbol, - ModelSeed originalModelSeed, ModelSeed.Builder builder) { - var originalSeed = originalModelSeed.getSeed(partialSymbol); - builder.seed(partialSymbol, new CancellableSeed<>(cancellationToken, originalSeed)); - } - - private record CancellableCursor<T>(CancellationToken cancellationToken, Cursor<Tuple, T> cursor) - implements Cursor<Tuple, T> { - @Override - public Tuple getKey() { - return cursor.getKey(); - } - - @Override - public T getValue() { - return cursor.getValue(); - } - - @Override - public boolean isTerminated() { - return cursor.isTerminated(); - } - - @Override - public boolean move() { - cancellationToken.checkCancelled(); - return cursor.move(); - } - - @Override - public boolean isDirty() { - return cursor.isDirty(); - } - - @Override - public Set<AnyVersionedMap> getDependingMaps() { - return cursor.getDependingMaps(); - } - } -} 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 c745d86e..33b1c4fb 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 @@ -34,7 +34,7 @@ import tools.refinery.store.reasoning.representation.PartialRelation; import tools.refinery.store.reasoning.translator.TranslationException; import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; import tools.refinery.store.tuple.Tuple; -import tools.refinery.viatra.runtime.CancellationToken; +import tools.refinery.store.util.CancellationToken; import java.util.ArrayList; import java.util.TreeMap; @@ -74,8 +74,8 @@ class SemanticsWorker implements Callable<SemanticsResult> { @Override public SemanticsResult call() { var builder = ModelStore.builder() - .with(ViatraModelQueryAdapter.builder() - .cancellationToken(cancellationToken)) + .cancellationToken(cancellationToken) + .with(ViatraModelQueryAdapter.builder()) .with(PropagationAdapter.builder()) .with(ReasoningAdapter.builder() .requiredInterpretations(Concreteness.PARTIAL)); @@ -91,8 +91,7 @@ class SemanticsWorker implements Callable<SemanticsResult> { cancellationToken.checkCancelled(); var store = builder.build(); cancellationToken.checkCancelled(); - var cancellableModelSeed = CancellableSeed.wrap(cancellationToken, modelSeed); - var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(cancellableModelSeed); + var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); cancellationToken.checkCancelled(); var partialInterpretation = getPartialInterpretation(initializer, model); diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/propagation/impl/PropagationAdapterImpl.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/propagation/impl/PropagationAdapterImpl.java index 586a8d7a..fdd19217 100644 --- a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/propagation/impl/PropagationAdapterImpl.java +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/propagation/impl/PropagationAdapterImpl.java @@ -31,6 +31,7 @@ class PropagationAdapterImpl implements PropagationAdapter { PropagationResult result = PropagationResult.UNCHANGED; PropagationResult lastResult; do { + model.checkCancelled(); lastResult = propagateOne(); result = result.andThen(lastResult); } while (lastResult.isChanged()); @@ -40,6 +41,7 @@ class PropagationAdapterImpl implements PropagationAdapter { private PropagationResult propagateOne() { PropagationResult result = PropagationResult.UNCHANGED; for (int i = 0; i < boundPropagators.length; i++) { + model.checkCancelled(); var lastResult = propagateUntilFixedPoint(i); result = result.andThen(lastResult); if (result.isRejected()) { @@ -54,6 +56,7 @@ class PropagationAdapterImpl implements PropagationAdapter { PropagationResult result = PropagationResult.UNCHANGED; PropagationResult lastResult; do { + model.checkCancelled(); lastResult = propagator.propagateOne(); result = result.andThen(lastResult); } while (lastResult.isChanged()); diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/propagation/impl/rule/BoundPropagationRule.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/propagation/impl/rule/BoundPropagationRule.java index 6e6a78d2..a70292ad 100644 --- a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/propagation/impl/rule/BoundPropagationRule.java +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/propagation/impl/rule/BoundPropagationRule.java @@ -13,10 +13,12 @@ import tools.refinery.store.query.ModelQueryAdapter; import tools.refinery.store.query.resultset.ResultSet; class BoundPropagationRule { + private final Model model; private final ResultSet<Boolean> resultSet; private final BoundAction action; public BoundPropagationRule(Model model, Rule rule) { + this.model = model; resultSet = model.getAdapter(ModelQueryAdapter.class).getResultSet(rule.getPrecondition()); action = rule.createAction(model); } @@ -27,6 +29,7 @@ class BoundPropagationRule { } var cursor = resultSet.getAll(); while (cursor.move()) { + model.checkCancelled(); var result = action.fire(cursor.getKey()); if (!result) { return PropagationResult.REJECTED; diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstExplorer.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstExplorer.java index 4a75a3a6..5e2f8fa9 100644 --- a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstExplorer.java +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstExplorer.java @@ -19,14 +19,9 @@ public class BestFirstExplorer extends BestFirstWorker { this.random = new Random(id); } - private boolean interrupted = false; - - public void interrupt() { - this.interrupted = true; - } - private boolean shouldRun() { - return !interrupted && !hasEnoughSolution(); + model.checkCancelled(); + return !hasEnoughSolution(); } public void explore() { diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/transition/actions/BoundAction.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/transition/actions/BoundAction.java index ed2ff33d..4da609fa 100644 --- a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/transition/actions/BoundAction.java +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/transition/actions/BoundAction.java @@ -23,6 +23,7 @@ public class BoundAction { } public boolean fire(Tuple activation) { + model.checkCancelled(); if (this.activation != null) { throw new IllegalStateException("Reentrant firing is not allowed"); } diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/transition/internal/DesignSpaceExplorationAdapterImpl.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/transition/internal/DesignSpaceExplorationAdapterImpl.java index e1a29d40..23325a1f 100644 --- a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/transition/internal/DesignSpaceExplorationAdapterImpl.java +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/transition/internal/DesignSpaceExplorationAdapterImpl.java @@ -56,6 +56,7 @@ public class DesignSpaceExplorationAdapterImpl implements DesignSpaceExploration @Override public boolean checkAccept() { for (var accept : this.accepts) { + model.checkCancelled(); if (!accept.isSatisfied()) { return false; } @@ -66,6 +67,7 @@ public class DesignSpaceExplorationAdapterImpl implements DesignSpaceExploration @Override public boolean checkExclude() { for (var exclude : this.excludes) { + model.checkCancelled(); if (exclude.isSatisfied()) { return true; } @@ -75,6 +77,7 @@ public class DesignSpaceExplorationAdapterImpl implements DesignSpaceExploration @Override public ObjectiveValue getObjectiveValue() { + model.checkCancelled(); if (objectives.size() == 1) { return ObjectiveValue.of(objectives.get(0).getValue()); } else if (objectives.size() == 2) { @@ -82,6 +85,7 @@ public class DesignSpaceExplorationAdapterImpl implements DesignSpaceExploration } else { double[] res = new double[objectives.size()]; for (int i = 0; i < objectives.size(); i++) { + model.checkCancelled(); res[i] = objectives.get(i).getValue(); } return ObjectiveValue.of(res); diff --git a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/ViatraModelQueryBuilder.java b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/ViatraModelQueryBuilder.java index 6b3be115..d19c3bb4 100644 --- a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/ViatraModelQueryBuilder.java +++ b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/ViatraModelQueryBuilder.java @@ -10,7 +10,6 @@ import tools.refinery.store.query.ModelQueryBuilder; import tools.refinery.store.query.dnf.AnyQuery; import tools.refinery.store.query.dnf.Dnf; import tools.refinery.store.query.rewriter.DnfRewriter; -import tools.refinery.viatra.runtime.CancellationToken; import tools.refinery.viatra.runtime.api.ViatraQueryEngineOptions; import tools.refinery.viatra.runtime.matchers.backend.IQueryBackendFactory; import tools.refinery.viatra.runtime.matchers.backend.QueryEvaluationHint; @@ -30,8 +29,6 @@ public interface ViatraModelQueryBuilder extends ModelQueryBuilder { ViatraModelQueryBuilder searchBackend(IQueryBackendFactory queryBackendFactory); - ViatraModelQueryBuilder cancellationToken(CancellationToken cancellationToken); - @Override default ViatraModelQueryBuilder queries(AnyQuery... queries) { ModelQueryBuilder.super.queries(queries); diff --git a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryBuilderImpl.java b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryBuilderImpl.java index bb0630f3..c68152e3 100644 --- a/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryBuilderImpl.java +++ b/subprojects/store-query-viatra/src/main/java/tools/refinery/store/query/viatra/internal/ViatraModelQueryBuilderImpl.java @@ -17,7 +17,6 @@ import tools.refinery.store.query.viatra.ViatraModelQueryBuilder; import tools.refinery.store.query.viatra.internal.localsearch.FlatCostFunction; import tools.refinery.store.query.viatra.internal.matcher.RawPatternMatcher; import tools.refinery.store.query.viatra.internal.pquery.Dnf2PQuery; -import tools.refinery.viatra.runtime.CancellationToken; import tools.refinery.viatra.runtime.api.IQuerySpecification; import tools.refinery.viatra.runtime.api.ViatraQueryEngineOptions; import tools.refinery.viatra.runtime.localsearch.matcher.integration.LocalSearchGenericBackendFactory; @@ -36,7 +35,6 @@ public class ViatraModelQueryBuilderImpl extends AbstractModelAdapterBuilder<Via // Use a cost function that ignores the initial (empty) model but allows higher arity input keys. LocalSearchHintOptions.PLANNER_COST_FUNCTION, new FlatCostFunction() ), (IQueryBackendFactory) null); - private CancellationToken cancellationToken = CancellationToken.NONE; private final CompositeRewriter rewriter; private final Dnf2PQuery dnf2PQuery = new Dnf2PQuery(); private final Set<AnyQuery> queries = new LinkedHashSet<>(); @@ -86,12 +84,6 @@ public class ViatraModelQueryBuilderImpl extends AbstractModelAdapterBuilder<Via return this; } - @Override - public ViatraModelQueryBuilder cancellationToken(CancellationToken cancellationToken) { - this.cancellationToken = cancellationToken; - return this; - } - @Override public ViatraModelQueryBuilder queries(Collection<? extends AnyQuery> queries) { checkNotConfigured(); @@ -144,7 +136,7 @@ public class ViatraModelQueryBuilderImpl extends AbstractModelAdapterBuilder<Via validateSymbols(store); return new ViatraModelQueryStoreAdapterImpl(store, buildEngineOptions(), dnf2PQuery.getSymbolViews(), Collections.unmodifiableMap(canonicalQueryMap), Collections.unmodifiableMap(querySpecifications), - Collections.unmodifiableSet(vacuousQueries), cancellationToken); + Collections.unmodifiableSet(vacuousQueries), store::checkCancelled); } private ViatraQueryEngineOptions buildEngineOptions() { diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java index 62aadb4a..3ae4d84b 100644 --- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java +++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/BoundScopePropagator.java @@ -22,6 +22,7 @@ import tools.refinery.store.representation.cardinality.*; import tools.refinery.store.tuple.Tuple; class BoundScopePropagator implements BoundPropagator { + private final Model model; private final ModelQueryAdapter queryEngine; private final Interpretation<CardinalityInterval> countInterpretation; private final MPSolver solver; @@ -32,6 +33,7 @@ class BoundScopePropagator implements BoundPropagator { private boolean changed = true; public BoundScopePropagator(Model model, ScopePropagator storeAdapter) { + this.model = model; queryEngine = model.getAdapter(ModelQueryAdapter.class); countInterpretation = model.getInterpretation(storeAdapter.getCountSymbol()); solver = MPSolver.createSolver("GLOP"); @@ -41,6 +43,7 @@ class BoundScopePropagator implements BoundPropagator { var propagatorFactories = storeAdapter.getTypeScopePropagatorFactories(); propagators = new TypeScopePropagator[propagatorFactories.size()]; for (int i = 0; i < propagators.length; i++) { + model.checkCancelled(); propagators[i] = propagatorFactories.get(i).createPropagator(this); } } @@ -145,6 +148,7 @@ class BoundScopePropagator implements BoundPropagator { } changed = false; for (var propagator : propagators) { + model.checkCancelled(); propagator.updateBounds(); } var result = PropagationResult.UNCHANGED; @@ -167,6 +171,7 @@ class BoundScopePropagator implements BoundPropagator { } private PropagationResult checkEmptiness() { + model.checkCancelled(); var emptinessCheckingResult = solver.solve(); return switch (emptinessCheckingResult) { case OPTIMAL, UNBOUNDED -> PropagationResult.UNCHANGED; @@ -178,6 +183,7 @@ class BoundScopePropagator implements BoundPropagator { private PropagationResult propagateNode(int nodeId, MPVariable variable) { objective.setCoefficient(variable, 1); try { + model.checkCancelled(); objective.setMinimization(); var minimizationResult = solver.solve(); int lowerBound; @@ -191,6 +197,7 @@ class BoundScopePropagator implements BoundPropagator { .formatted(variable, minimizationResult)); } + model.checkCancelled(); objective.setMaximization(); var maximizationResult = solver.solve(); UpperCardinality upperBound; diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiobject/MultiObjectInitializer.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiobject/MultiObjectInitializer.java index f11ab46b..89918155 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiobject/MultiObjectInitializer.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/multiobject/MultiObjectInitializer.java @@ -30,9 +30,9 @@ class MultiObjectInitializer implements PartialModelInitializer { @Override public void initialize(Model model, ModelSeed modelSeed) { - var intervals = initializeIntervals(modelSeed); - initializeExists(intervals, modelSeed); - initializeEquals(intervals, modelSeed); + var intervals = initializeIntervals(model, modelSeed); + initializeExists(intervals, model, modelSeed); + initializeEquals(intervals, model, modelSeed); var countInterpretation = model.getInterpretation(countSymbol); var uniqueTable = new HashMap<CardinalityInterval, CardinalityInterval>(); for (int i = 0; i < intervals.length; i++) { @@ -47,12 +47,13 @@ class MultiObjectInitializer implements PartialModelInitializer { } @NotNull - private CardinalityInterval[] initializeIntervals(ModelSeed modelSeed) { + private CardinalityInterval[] initializeIntervals(Model model, ModelSeed modelSeed) { var intervals = new CardinalityInterval[modelSeed.getNodeCount()]; if (modelSeed.containsSeed(MultiObjectTranslator.COUNT_SYMBOL)) { Arrays.fill(intervals, CardinalityIntervals.ONE); var cursor = modelSeed.getCursor(MultiObjectTranslator.COUNT_SYMBOL, CardinalityIntervals.ONE); while (cursor.move()) { + model.checkCancelled(); int i = cursor.getKey().get(0); checkNodeId(intervals, i); intervals[i] = cursor.getValue(); @@ -70,12 +71,13 @@ class MultiObjectInitializer implements PartialModelInitializer { return intervals; } - private void initializeExists(CardinalityInterval[] intervals, ModelSeed modelSeed) { + private void initializeExists(CardinalityInterval[] intervals, Model model, ModelSeed modelSeed) { if (!modelSeed.containsSeed(ReasoningAdapter.EXISTS_SYMBOL)) { return; } var cursor = modelSeed.getCursor(ReasoningAdapter.EXISTS_SYMBOL, TruthValue.UNKNOWN); while (cursor.move()) { + model.checkCancelled(); int i = cursor.getKey().get(0); checkNodeId(intervals, i); switch (cursor.getValue()) { @@ -89,13 +91,14 @@ class MultiObjectInitializer implements PartialModelInitializer { } } - private void initializeEquals(CardinalityInterval[] intervals, ModelSeed modelSeed) { + private void initializeEquals(CardinalityInterval[] intervals, Model model, ModelSeed modelSeed) { if (!modelSeed.containsSeed(ReasoningAdapter.EQUALS_SYMBOL)) { return; } var seed = modelSeed.getSeed(ReasoningAdapter.EQUALS_SYMBOL); var cursor = seed.getCursor(TruthValue.FALSE, modelSeed.getNodeCount()); while (cursor.move()) { + model.checkCancelled(); var key = cursor.getKey(); int i = key.get(0); int otherIndex = key.get(1); @@ -116,6 +119,7 @@ class MultiObjectInitializer implements PartialModelInitializer { } } for (int i = 0; i < intervals.length; i++) { + model.checkCancelled(); if (seed.get(Tuple.of(i, i)) == TruthValue.FALSE) { throw new TranslationException(ReasoningAdapter.EQUALS_SYMBOL, "Inconsistent equality for node " + i); } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyInitializer.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyInitializer.java index c74f1e78..233e43f0 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyInitializer.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/typehierarchy/TypeHierarchyInitializer.java @@ -31,20 +31,23 @@ public class TypeHierarchyInitializer implements PartialModelInitializer { var inferredTypes = new InferredType[modelSeed.getNodeCount()]; Arrays.fill(inferredTypes, typeHierarchy.getUnknownType()); for (var type : typeHierarchy.getAllTypes()) { - initializeType(type, inferredTypes, modelSeed); + model.checkCancelled(); + initializeType(type, inferredTypes, model, modelSeed); } var typeInterpretation = model.getInterpretation(typeSymbol); var uniqueTable = new HashMap<InferredType, InferredType>(); for (int i = 0; i < inferredTypes.length; i++) { + model.checkCancelled(); var uniqueType = uniqueTable.computeIfAbsent(inferredTypes[i], Function.identity()); typeInterpretation.put(Tuple.of(i), uniqueType); } } - private void initializeType(PartialRelation type, InferredType[] inferredTypes, ModelSeed modelSeed) { + private void initializeType(PartialRelation type, InferredType[] inferredTypes, Model model, ModelSeed modelSeed) { var cursor = modelSeed.getCursor(type, TruthValue.UNKNOWN); var analysisResult = typeHierarchy.getAnalysisResult(type); while (cursor.move()) { + model.checkCancelled(); var i = cursor.getKey().get(0); checkNodeId(inferredTypes, i); var value = cursor.getValue(); diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/Model.java b/subprojects/store/src/main/java/tools/refinery/store/model/Model.java index e2ab72e7..c4ce5207 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/Model.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/Model.java @@ -8,11 +8,9 @@ package tools.refinery.store.model; import tools.refinery.store.adapter.ModelAdapter; import tools.refinery.store.map.Version; import tools.refinery.store.map.Versioned; -import tools.refinery.store.model.internal.VersionedInterpretation; import tools.refinery.store.representation.AnySymbol; import tools.refinery.store.representation.Symbol; -import java.util.Map; import java.util.Optional; public interface Model extends Versioned { @@ -38,4 +36,6 @@ public interface Model extends Versioned { void addListener(ModelListener listener); void removeListener(ModelListener listener); + + void checkCancelled(); } diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/ModelStore.java b/subprojects/store/src/main/java/tools/refinery/store/model/ModelStore.java index 89382b3a..61abf126 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/ModelStore.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/ModelStore.java @@ -26,6 +26,8 @@ public interface ModelStore { <T extends ModelStoreAdapter> T getAdapter(Class<T> adapterType); + void checkCancelled(); + static ModelStoreBuilder builder() { return new ModelStoreBuilderImpl(); } diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/ModelStoreBuilder.java b/subprojects/store/src/main/java/tools/refinery/store/model/ModelStoreBuilder.java index 8f652762..9b2b38c3 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/ModelStoreBuilder.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/ModelStoreBuilder.java @@ -8,12 +8,15 @@ package tools.refinery.store.model; import tools.refinery.store.adapter.ModelAdapterBuilder; import tools.refinery.store.representation.AnySymbol; import tools.refinery.store.representation.Symbol; +import tools.refinery.store.util.CancellationToken; import java.util.Collection; import java.util.List; import java.util.Optional; public interface ModelStoreBuilder { + ModelStoreBuilder cancellationToken(CancellationToken cancellationToken); + default ModelStoreBuilder symbols(AnySymbol... symbols) { return symbols(List.of(symbols)); } diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelImpl.java b/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelImpl.java index 2b12d5a6..d11e431b 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelImpl.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelImpl.java @@ -13,23 +13,26 @@ import tools.refinery.store.model.*; import tools.refinery.store.representation.AnySymbol; import tools.refinery.store.representation.Symbol; import tools.refinery.store.tuple.Tuple; +import tools.refinery.store.util.CancellationToken; import java.util.*; public class ModelImpl implements Model { - private final ModelStore store; + private final ModelStoreImpl store; private Version state; private LinkedHashMap<? extends AnySymbol, ? extends VersionedInterpretation<?>> interpretations; private final List<ModelAdapter> adapters; private final List<ModelListener> listeners = new ArrayList<>(); + private final CancellationToken cancellationToken; private boolean uncommittedChanges; private ModelAction pendingAction = ModelAction.NONE; private Version restoringToState = null; - ModelImpl(ModelStore store, Version state, int adapterCount) { + ModelImpl(ModelStoreImpl store, Version state, int adapterCount) { this.store = store; this.state = state; adapters = new ArrayList<>(adapterCount); + cancellationToken = store.getCancellationToken(); } void setInterpretations(LinkedHashMap<? extends AnySymbol, ? extends VersionedInterpretation<?>> interpretations) { @@ -88,6 +91,7 @@ public class ModelImpl implements Model { @Override public Version commit() { + checkCancelled(); if (hasPendingAction()) { throw pendingActionError("commit"); } @@ -106,6 +110,7 @@ public class ModelImpl implements Model { Version[] interpretationVersions = new Version[interpretations.size()]; int j = 0; for (var interpretationEntry : interpretations.entrySet()) { + checkCancelled(); interpretationVersions[j++] = interpretationEntry.getValue().commit(); } ModelVersion modelVersion = new ModelVersion(interpretationVersions); @@ -125,6 +130,7 @@ public class ModelImpl implements Model { @Override public void restore(Version version) { + checkCancelled(); if (hasPendingAction()) { throw pendingActionError("restore to %s".formatted(version)); } @@ -140,6 +146,7 @@ public class ModelImpl implements Model { } int j = 0; for (var interpretation : interpretations.values()) { + checkCancelled(); interpretation.restore(ModelVersion.getInternalVersion(version, j++)); } @@ -187,4 +194,9 @@ public class ModelImpl implements Model { public void removeListener(ModelListener listener) { listeners.remove(listener); } + + @Override + public void checkCancelled() { + cancellationToken.checkCancelled(); + } } diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelStoreBuilderImpl.java b/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelStoreBuilderImpl.java index 2dde7a4c..53ecde5e 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelStoreBuilderImpl.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelStoreBuilderImpl.java @@ -16,14 +16,28 @@ import tools.refinery.store.model.ModelStoreConfiguration; import tools.refinery.store.representation.AnySymbol; import tools.refinery.store.representation.Symbol; import tools.refinery.store.tuple.Tuple; +import tools.refinery.store.util.CancellationToken; import java.util.*; public class ModelStoreBuilderImpl implements ModelStoreBuilder { + private CancellationToken cancellationToken; private final LinkedHashSet<AnySymbol> allSymbols = new LinkedHashSet<>(); private final LinkedHashMap<SymbolEquivalenceClass<?>, List<AnySymbol>> equivalenceClasses = new LinkedHashMap<>(); private final List<ModelAdapterBuilder> adapters = new ArrayList<>(); + @Override + public ModelStoreBuilder cancellationToken(CancellationToken cancellationToken) { + if (this.cancellationToken != null) { + throw new IllegalStateException("Cancellation token was already set"); + } + if (cancellationToken == null) { + throw new IllegalStateException("Cancellation token must not be null"); + } + this.cancellationToken = cancellationToken; + return this; + } + @Override public <T> ModelStoreBuilder symbol(Symbol<T> symbol) { if (!allSymbols.add(symbol)) { @@ -75,7 +89,8 @@ public class ModelStoreBuilderImpl implements ModelStoreBuilder { for (var entry : equivalenceClasses.entrySet()) { createStores(stores, entry.getKey(), entry.getValue()); } - var modelStore = new ModelStoreImpl(stores, adapters.size()); + var modelStore = new ModelStoreImpl(stores, adapters.size(), cancellationToken == null ? + CancellationToken.NONE : cancellationToken); for (var adapterBuilder : adapters) { var storeAdapter = adapterBuilder.build(modelStore); modelStore.addAdapter(storeAdapter); @@ -86,8 +101,8 @@ public class ModelStoreBuilderImpl implements ModelStoreBuilder { private <T> void createStores(Map<AnySymbol, VersionedMapStore<Tuple, ?>> stores, SymbolEquivalenceClass<T> equivalenceClass, List<AnySymbol> symbols) { int size = symbols.size(); - VersionedMapStoreFactory<Tuple,T> mapFactory = VersionedMapStore - .<Tuple,T>builder() + VersionedMapStoreFactory<Tuple, T> mapFactory = VersionedMapStore + .<Tuple, T>builder() .strategy(VersionedMapStoreFactoryBuilder.StoreStrategy.DELTA) .defaultValue(equivalenceClass.defaultValue()) .build(); diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelStoreImpl.java b/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelStoreImpl.java index a320a618..fd4cc160 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelStoreImpl.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/internal/ModelStoreImpl.java @@ -14,16 +14,20 @@ import tools.refinery.store.model.ModelDiffCursor; import tools.refinery.store.model.ModelStore; import tools.refinery.store.representation.AnySymbol; import tools.refinery.store.tuple.Tuple; +import tools.refinery.store.util.CancellationToken; import java.util.*; public class ModelStoreImpl implements ModelStore { private final LinkedHashMap<? extends AnySymbol, ? extends VersionedMapStore<Tuple, ?>> stores; private final List<ModelStoreAdapter> adapters; + private final CancellationToken cancellationToken; - ModelStoreImpl(LinkedHashMap<? extends AnySymbol, ? extends VersionedMapStore<Tuple, ?>> stores, int adapterCount) { + ModelStoreImpl(LinkedHashMap<? extends AnySymbol, ? extends VersionedMapStore<Tuple, ?>> stores, int adapterCount, + CancellationToken cancellationToken) { this.stores = stores; adapters = new ArrayList<>(adapterCount); + this.cancellationToken = cancellationToken; } @Override @@ -100,4 +104,13 @@ public class ModelStoreImpl implements ModelStore { void addAdapter(ModelStoreAdapter adapter) { adapters.add(adapter); } + + @Override + public void checkCancelled() { + cancellationToken.checkCancelled(); + } + + CancellationToken getCancellationToken() { + return cancellationToken; + } } diff --git a/subprojects/store/src/main/java/tools/refinery/store/model/internal/VersionedInterpretation.java b/subprojects/store/src/main/java/tools/refinery/store/model/internal/VersionedInterpretation.java index 71df3962..dcf0ad08 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/model/internal/VersionedInterpretation.java +++ b/subprojects/store/src/main/java/tools/refinery/store/model/internal/VersionedInterpretation.java @@ -75,6 +75,7 @@ public abstract class VersionedInterpretation<T> implements Interpretation<T> { @Override public T put(Tuple key, T value) { checkKey(key); + model.checkCancelled(); model.markAsChanged(); var oldValue = map.put(key, value); valueChanged(key, oldValue, value, false); @@ -83,15 +84,12 @@ public abstract class VersionedInterpretation<T> implements Interpretation<T> { @Override public void putAll(Cursor<Tuple, T> cursor) { - if (listeners.isEmpty()) { - map.putAll(cursor); - return; - } model.markAsChanged(); if (cursor.getDependingMaps().contains(map)) { List<Tuple> keys = new ArrayList<>(); List<T> values = new ArrayList<>(); while (cursor.move()) { + model.checkCancelled(); keys.add(cursor.getKey()); values.add(cursor.getValue()); } diff --git a/subprojects/store/src/main/java/tools/refinery/store/statecoding/StateCodeCalculatorFactory.java b/subprojects/store/src/main/java/tools/refinery/store/statecoding/StateCodeCalculatorFactory.java index 04e17a13..809205e4 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/statecoding/StateCodeCalculatorFactory.java +++ b/subprojects/store/src/main/java/tools/refinery/store/statecoding/StateCodeCalculatorFactory.java @@ -7,9 +7,10 @@ package tools.refinery.store.statecoding; import org.eclipse.collections.api.set.primitive.IntSet; import tools.refinery.store.model.Interpretation; +import tools.refinery.store.model.Model; import java.util.List; public interface StateCodeCalculatorFactory { - StateCodeCalculator create(List<? extends Interpretation<?>> interpretations, IntSet individuals); + StateCodeCalculator create(Model model, List<? extends Interpretation<?>> interpretations, IntSet individuals); } diff --git a/subprojects/store/src/main/java/tools/refinery/store/statecoding/internal/StateCoderStoreAdapterImpl.java b/subprojects/store/src/main/java/tools/refinery/store/statecoding/internal/StateCoderStoreAdapterImpl.java index 89586bfb..2cb6f3c1 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/statecoding/internal/StateCoderStoreAdapterImpl.java +++ b/subprojects/store/src/main/java/tools/refinery/store/statecoding/internal/StateCoderStoreAdapterImpl.java @@ -68,7 +68,7 @@ public class StateCoderStoreAdapterImpl implements StateCoderStoreAdapter { @Override public StateCoderAdapter createModelAdapter(Model model) { var interpretations = symbols.stream().map(model::getInterpretation).toList(); - var coder = codeCalculatorFactory.create(interpretations, individuals); + var coder = codeCalculatorFactory.create(model, interpretations, individuals); return new StateCoderAdapterImpl(this, coder, model); } } diff --git a/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/AbstractNeighbourhoodCalculator.java b/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/AbstractNeighbourhoodCalculator.java index 5d390da2..4cef6786 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/AbstractNeighbourhoodCalculator.java +++ b/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/AbstractNeighbourhoodCalculator.java @@ -10,6 +10,7 @@ import org.eclipse.collections.api.map.primitive.MutableIntLongMap; import org.eclipse.collections.api.set.primitive.IntSet; import tools.refinery.store.model.AnyInterpretation; import tools.refinery.store.model.Interpretation; +import tools.refinery.store.model.Model; import tools.refinery.store.statecoding.ObjectCode; import tools.refinery.store.tuple.Tuple; import tools.refinery.store.tuple.Tuple0; @@ -17,13 +18,16 @@ import tools.refinery.store.tuple.Tuple0; import java.util.*; public abstract class AbstractNeighbourhoodCalculator { + protected final Model model; protected final List<AnyInterpretation> nullImpactValues; protected final LinkedHashMap<AnyInterpretation, long[]> impactValues; protected final MutableIntLongMap individualHashValues = IntLongMaps.mutable.empty(); protected static final long PRIME = 31; - protected AbstractNeighbourhoodCalculator(List<? extends AnyInterpretation> interpretations, IntSet individuals) { + protected AbstractNeighbourhoodCalculator(Model model, List<? extends AnyInterpretation> interpretations, + IntSet individuals) { + this.model = model; this.nullImpactValues = new ArrayList<>(); this.impactValues = new LinkedHashMap<>(); // Random isn't used for cryptographical purposes but just to assign distinguishable identifiers to symbols. diff --git a/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/LazyNeighbourhoodCalculator.java b/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/LazyNeighbourhoodCalculator.java index c188a839..04335141 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/LazyNeighbourhoodCalculator.java +++ b/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/LazyNeighbourhoodCalculator.java @@ -12,6 +12,7 @@ import org.eclipse.collections.api.set.primitive.IntSet; import tools.refinery.store.map.Cursor; import tools.refinery.store.model.AnyInterpretation; import tools.refinery.store.model.Interpretation; +import tools.refinery.store.model.Model; import tools.refinery.store.statecoding.StateCodeCalculator; import tools.refinery.store.statecoding.StateCoderResult; import tools.refinery.store.tuple.Tuple; @@ -19,8 +20,9 @@ import tools.refinery.store.tuple.Tuple; import java.util.List; public class LazyNeighbourhoodCalculator extends AbstractNeighbourhoodCalculator implements StateCodeCalculator { - public LazyNeighbourhoodCalculator(List<? extends AnyInterpretation> interpretations, IntSet individuals) { - super(interpretations, individuals); + public LazyNeighbourhoodCalculator(Model model, List<? extends AnyInterpretation> interpretations, + IntSet individuals) { + super(model, interpretations, individuals); } public StateCoderResult calculateCodes() { diff --git a/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/NeighbourhoodCalculator.java b/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/NeighbourhoodCalculator.java index 1442c915..5e6de53b 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/NeighbourhoodCalculator.java +++ b/subprojects/store/src/main/java/tools/refinery/store/statecoding/neighbourhood/NeighbourhoodCalculator.java @@ -8,6 +8,7 @@ package tools.refinery.store.statecoding.neighbourhood; import org.eclipse.collections.api.set.primitive.IntSet; import tools.refinery.store.map.Cursor; import tools.refinery.store.model.Interpretation; +import tools.refinery.store.model.Model; import tools.refinery.store.statecoding.ObjectCode; import tools.refinery.store.statecoding.StateCodeCalculator; import tools.refinery.store.statecoding.StateCoderResult; @@ -21,17 +22,19 @@ public class NeighbourhoodCalculator extends AbstractNeighbourhoodCalculator imp private ObjectCodeImpl previousObjectCode = new ObjectCodeImpl(); private ObjectCodeImpl nextObjectCode = new ObjectCodeImpl(); - public NeighbourhoodCalculator(List<? extends Interpretation<?>> interpretations, IntSet individuals) { - super(interpretations, individuals); + public NeighbourhoodCalculator(Model model, List<? extends Interpretation<?>> interpretations, IntSet individuals) { + super(model, interpretations, individuals); } public StateCoderResult calculateCodes() { + model.checkCancelled(); previousObjectCode.clear(); nextObjectCode.clear(); initializeWithIndividuals(previousObjectCode); int rounds = 0; do { + model.checkCancelled(); constructNextObjectCodes(previousObjectCode, nextObjectCode); var tempObjectCode = previousObjectCode; previousObjectCode = nextObjectCode; @@ -60,6 +63,7 @@ public class NeighbourhoodCalculator extends AbstractNeighbourhoodCalculator imp private void constructNextObjectCodes(ObjectCodeImpl previous, ObjectCodeImpl next) { for (var impactValueEntry : this.impactValues.entrySet()) { + model.checkCancelled(); Interpretation<?> interpretation = (Interpretation<?>) impactValueEntry.getKey(); var cursor = interpretation.getAll(); int arity = interpretation.getSymbol().arity(); diff --git a/subprojects/store/src/main/java/tools/refinery/store/util/CancellationToken.java b/subprojects/store/src/main/java/tools/refinery/store/util/CancellationToken.java new file mode 100644 index 00000000..be294013 --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/util/CancellationToken.java @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.util; + +@FunctionalInterface +public interface CancellationToken { + CancellationToken NONE = () -> {}; + + void checkCancelled(); +} diff --git a/subprojects/store/src/test/java/tools/refinery/store/statecoding/EquivalenceTest.java b/subprojects/store/src/test/java/tools/refinery/store/statecoding/EquivalenceTest.java index 3c35849e..f2d2f7b5 100644 --- a/subprojects/store/src/test/java/tools/refinery/store/statecoding/EquivalenceTest.java +++ b/subprojects/store/src/test/java/tools/refinery/store/statecoding/EquivalenceTest.java @@ -192,7 +192,8 @@ class EquivalenceTest { ModelStore store = ModelStore.builder() .symbols(person, age, friend, parents, population) .with(StateCoderAdapter.builder() - .stateCodeCalculatorFactory((p1, p2) -> calculator)) + .stateCodeCalculatorFactory((ignoredModel, ignoredInterpretations, ignoredIndividuals) -> + calculator)) .build(); var stateCoder = store.getAdapter(StateCoderStoreAdapter.class); diff --git a/subprojects/store/src/test/java/tools/refinery/store/statecoding/StateCoderBuildTest.java b/subprojects/store/src/test/java/tools/refinery/store/statecoding/StateCoderBuildTest.java index 0b738005..0928aa8e 100644 --- a/subprojects/store/src/test/java/tools/refinery/store/statecoding/StateCoderBuildTest.java +++ b/subprojects/store/src/test/java/tools/refinery/store/statecoding/StateCoderBuildTest.java @@ -124,7 +124,8 @@ class StateCoderBuildTest { var store = ModelStore.builder() .symbols(friend) .with(StateCoderAdapter.builder() - .stateCodeCalculatorFactory((interpretations, individuals) -> mock)) + .stateCodeCalculatorFactory((ignoredModel, ignoredInterpretations, ignoredIndividuals) -> + mock)) .build(); var model = store.createEmptyModel(); -- cgit v1.2.3-54-g00ecf From a2a4696fdbd6440269d576aeba7b25b2ea40d9bf Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Tue, 12 Sep 2023 21:59:50 +0200 Subject: feat: connect model generator to UI --- subprojects/frontend/src/ModelWorkArea.tsx | 198 ++++++++++++++++++ subprojects/frontend/src/WorkArea.tsx | 12 +- subprojects/frontend/src/editor/EditorStore.ts | 85 ++++++++ subprojects/frontend/src/editor/GenerateButton.tsx | 20 +- .../frontend/src/editor/GeneratedModelStore.ts | 50 +++++ subprojects/frontend/src/graph/GraphArea.tsx | 12 +- subprojects/frontend/src/graph/GraphPane.tsx | 10 +- subprojects/frontend/src/index.tsx | 102 +++++++-- subprojects/frontend/src/table/RelationGrid.tsx | 109 ---------- subprojects/frontend/src/table/TableArea.tsx | 105 +++++++++- subprojects/frontend/src/table/TablePane.tsx | 9 +- .../frontend/src/xtext/ModelGenerationService.ts | 46 +++++ subprojects/frontend/src/xtext/UpdateService.ts | 39 ++++ subprojects/frontend/src/xtext/XtextClient.ts | 21 +- subprojects/frontend/src/xtext/xtextMessages.ts | 1 + .../frontend/src/xtext/xtextServiceResults.ts | 24 +++ .../semantics/metadata/MetadataCreator.java | 12 +- .../generator/ModelGenerationCancelledResult.java | 11 + .../web/generator/ModelGenerationErrorResult.java | 11 + .../web/generator/ModelGenerationManager.java | 41 ++++ .../web/generator/ModelGenerationResult.java | 15 ++ .../web/generator/ModelGenerationService.java | 60 ++++++ .../generator/ModelGenerationStartedResult.java | 13 ++ .../web/generator/ModelGenerationStatusResult.java | 11 + .../generator/ModelGenerationSuccessResult.java | 17 ++ .../web/generator/ModelGenerationWorker.java | 228 +++++++++++++++++++++ .../web/semantics/PartialInterpretation2Json.java | 81 ++++++++ .../language/web/semantics/SemanticsService.java | 2 +- .../language/web/semantics/SemanticsWorker.java | 63 +----- .../server/ThreadPoolExecutorServiceProvider.java | 32 ++- .../web/xtext/server/TransactionExecutor.java | 2 +- .../xtext/server/push/PushServiceDispatcher.java | 39 ++++ .../web/xtext/server/push/PushWebDocument.java | 20 +- .../xtext/server/push/PushWebDocumentAccess.java | 4 + .../refinery/store/reasoning/ReasoningAdapter.java | 2 + .../reasoning/internal/ReasoningAdapterImpl.java | 6 + 36 files changed, 1278 insertions(+), 235 deletions(-) create mode 100644 subprojects/frontend/src/ModelWorkArea.tsx create mode 100644 subprojects/frontend/src/editor/GeneratedModelStore.ts delete mode 100644 subprojects/frontend/src/table/RelationGrid.tsx create mode 100644 subprojects/frontend/src/xtext/ModelGenerationService.ts create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java create mode 100644 subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/frontend/src/ModelWorkArea.tsx b/subprojects/frontend/src/ModelWorkArea.tsx new file mode 100644 index 00000000..3aba31e3 --- /dev/null +++ b/subprojects/frontend/src/ModelWorkArea.tsx @@ -0,0 +1,198 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import CloseIcon from '@mui/icons-material/Close'; +import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied'; +import CircularProgress from '@mui/material/CircularProgress'; +import IconButton from '@mui/material/IconButton'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import { styled } from '@mui/material/styles'; +import { observer } from 'mobx-react-lite'; + +import DirectionalSplitPane from './DirectionalSplitPane'; +import Loading from './Loading'; +import { useRootStore } from './RootStoreProvider'; +import type GeneratedModelStore from './editor/GeneratedModelStore'; +import GraphPane from './graph/GraphPane'; +import type GraphStore from './graph/GraphStore'; +import TablePane from './table/TablePane'; +import type ThemeStore from './theme/ThemeStore'; + +const SplitGraphPane = observer(function SplitGraphPane({ + graph, + themeStore, +}: { + graph: GraphStore; + themeStore: ThemeStore; +}): JSX.Element { + return ( + <DirectionalSplitPane + primary={<GraphPane graph={graph} />} + secondary={<TablePane graph={graph} />} + primaryOnly={!themeStore.showTable} + secondaryOnly={!themeStore.showGraph} + /> + ); +}); + +const GenerationStatus = styled('div', { + name: 'ModelWorkArea-GenerationStatus', + shouldForwardProp: (prop) => prop !== 'error', +})<{ error: boolean }>(({ error, theme }) => ({ + color: error ? theme.palette.error.main : theme.palette.text.primary, + ...(error + ? { + fontWeight: theme.typography.fontWeightBold ?? 600, + } + : {}), +})); + +const GeneratedModelPane = observer(function GeneratedModelPane({ + generatedModel, + themeStore, +}: { + generatedModel: GeneratedModelStore; + themeStore: ThemeStore; +}): JSX.Element { + const { message, error, graph } = generatedModel; + + if (graph !== undefined) { + return <SplitGraphPane graph={graph} themeStore={themeStore} />; + } + + return ( + <Stack + direction="column" + alignItems="center" + justifyContent="center" + height="100%" + width="100%" + overflow="hidden" + my={2} + > + <Stack + direction="column" + alignItems="center" + flexGrow={1} + flexShrink={1} + flexBasis={0} + sx={(theme) => ({ + maxHeight: '6rem', + height: 'calc(100% - 8rem)', + marginBottom: theme.spacing(1), + padding: error ? 0 : theme.spacing(1), + color: theme.palette.text.secondary, + '.MuiCircularProgress-root, .MuiCircularProgress-svg, .MuiSvgIcon-root': + { + height: '100% !important', + width: '100% !important', + }, + })} + > + {error ? ( + <SentimentVeryDissatisfiedIcon + className="VisibilityDialog-emptyIcon" + fontSize="inherit" + /> + ) : ( + <CircularProgress color="inherit" /> + )} + </Stack> + <GenerationStatus error={error}>{message}</GenerationStatus> + </Stack> + ); +}); + +function ModelWorkArea(): JSX.Element { + const { editorStore, themeStore } = useRootStore(); + + if (editorStore === undefined) { + return <Loading />; + } + + const { graph, generatedModels, selectedGeneratedModel } = editorStore; + + const generatedModelNames: string[] = []; + const generatedModelTabs: JSX.Element[] = []; + generatedModels.forEach((value, key) => { + generatedModelNames.push(key); + /* eslint-disable react/no-array-index-key -- Key is a string here, not the array index. */ + generatedModelTabs.push( + <Tab + label={value.title} + key={key} + onAuxClick={(event) => { + if (event.button === 1) { + editorStore.deleteGeneratedModel(key); + event.preventDefault(); + event.stopPropagation(); + } + }} + />, + ); + /* eslint-enable react/no-array-index-key */ + }); + const generatedModel = + selectedGeneratedModel === undefined + ? undefined + : generatedModels.get(selectedGeneratedModel); + const selectedIndex = + selectedGeneratedModel === undefined + ? 0 + : generatedModelNames.indexOf(selectedGeneratedModel) + 1; + + return ( + <Stack direction="column" height="100%" width="100%" overflow="hidden"> + <Stack + direction="row" + sx={(theme) => ({ + display: generatedModelNames.length === 0 ? 'none' : 'flex', + alignItems: 'center', + borderBottom: `1px solid ${theme.palette.outer.border}`, + })} + > + <Tabs + value={selectedIndex} + onChange={(_event, value) => { + if (value === 0) { + editorStore.selectGeneratedModel(undefined); + } else if (typeof value === 'number') { + editorStore.selectGeneratedModel(generatedModelNames[value - 1]); + } + }} + sx={{ flexGrow: 1 }} + > + <Tab label="Initial model" /> + {generatedModelTabs} + </Tabs> + <IconButton + aria-label="Close generated model" + onClick={() => + editorStore.deleteGeneratedModel(selectedGeneratedModel) + } + sx={{ + display: selectedIndex === 0 ? 'none' : 'flex', + mx: 1, + }} + > + <CloseIcon fontSize="small" /> + </IconButton> + </Stack> + {generatedModel === undefined ? ( + <SplitGraphPane graph={graph} themeStore={themeStore} /> + ) : ( + <GeneratedModelPane + generatedModel={generatedModel} + themeStore={themeStore} + /> + )} + </Stack> + ); +} + +export default observer(ModelWorkArea); diff --git a/subprojects/frontend/src/WorkArea.tsx b/subprojects/frontend/src/WorkArea.tsx index adb29a50..a1fbf7dc 100644 --- a/subprojects/frontend/src/WorkArea.tsx +++ b/subprojects/frontend/src/WorkArea.tsx @@ -7,10 +7,9 @@ import { observer } from 'mobx-react-lite'; import DirectionalSplitPane from './DirectionalSplitPane'; +import ModelWorkArea from './ModelWorkArea'; import { useRootStore } from './RootStoreProvider'; import EditorPane from './editor/EditorPane'; -import GraphPane from './graph/GraphPane'; -import TablePane from './table/TablePane'; export default observer(function WorkArea(): JSX.Element { const { themeStore } = useRootStore(); @@ -18,14 +17,7 @@ export default observer(function WorkArea(): JSX.Element { return ( <DirectionalSplitPane primary={<EditorPane />} - secondary={ - <DirectionalSplitPane - primary={<GraphPane />} - secondary={<TablePane />} - primaryOnly={!themeStore.showTable} - secondaryOnly={!themeStore.showGraph} - /> - } + secondary={<ModelWorkArea />} primaryOnly={!themeStore.showGraph && !themeStore.showTable} secondaryOnly={!themeStore.showCode} /> diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index b5989ad1..f9a9a7da 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts @@ -32,6 +32,7 @@ import type XtextClient from '../xtext/XtextClient'; import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; import EditorErrors from './EditorErrors'; +import GeneratedModelStore from './GeneratedModelStore'; import LintPanelStore from './LintPanelStore'; import SearchPanelStore from './SearchPanelStore'; import createEditorState from './createEditorState'; @@ -69,6 +70,10 @@ export default class EditorStore { graph: GraphStore; + generatedModels = new Map<string, GeneratedModelStore>(); + + selectedGeneratedModel: string | undefined; + constructor(initialValue: string, pwaStore: PWAStore) { this.id = nanoid(); this.state = createEditorState(initialValue, this); @@ -307,4 +312,84 @@ export default class EditorStore { this.delayedErrors.dispose(); this.disposed = true; } + + startModelGeneration(): void { + this.client + ?.startModelGeneration() + ?.catch((error) => log.error('Could not start model generation', error)); + } + + addGeneratedModel(uuid: string): void { + this.generatedModels.set(uuid, new GeneratedModelStore()); + this.selectGeneratedModel(uuid); + } + + cancelModelGeneration(): void { + this.client + ?.cancelModelGeneration() + ?.catch((error) => log.error('Could not start model generation', error)); + } + + selectGeneratedModel(uuid: string | undefined): void { + if (uuid === undefined) { + this.selectedGeneratedModel = uuid; + return; + } + if (this.generatedModels.has(uuid)) { + this.selectedGeneratedModel = uuid; + return; + } + this.selectedGeneratedModel = undefined; + } + + deleteGeneratedModel(uuid: string | undefined): void { + if (uuid === undefined) { + return; + } + if (this.selectedGeneratedModel === uuid) { + let previous: string | undefined; + let found: string | undefined; + this.generatedModels.forEach((_value, key) => { + if (key === uuid) { + found = previous; + } + previous = key; + }); + this.selectGeneratedModel(found); + } + const generatedModel = this.generatedModels.get(uuid); + if (generatedModel !== undefined && generatedModel.running) { + this.cancelModelGeneration(); + } + this.generatedModels.delete(uuid); + } + + modelGenerationCancelled(): void { + this.generatedModels.forEach((value) => + value.setError('Model generation cancelled'), + ); + } + + setGeneratedModelMessage(uuid: string, message: string): void { + this.generatedModels.get(uuid)?.setMessage(message); + } + + setGeneratedModelError(uuid: string, message: string): void { + this.generatedModels.get(uuid)?.setError(message); + } + + setGeneratedModelSemantics( + uuid: string, + semantics: SemanticsSuccessResult, + ): void { + this.generatedModels.get(uuid)?.setSemantics(semantics); + } + + get generating(): boolean { + let generating = false; + this.generatedModels.forEach((value) => { + generating = generating || value.running; + }); + return generating; + } } diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 5bac0464..b8dcd531 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx @@ -5,6 +5,7 @@ */ import CancelIcon from '@mui/icons-material/Cancel'; +import CloseIcon from '@mui/icons-material/Close'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import { observer } from 'mobx-react-lite'; @@ -28,8 +29,10 @@ const GenerateButton = observer(function GenerateButton({ ); } - const { analyzing, errorCount, warningCount, semanticsError } = - editorStore.delayedErrors; + const { + delayedErrors: { analyzing, errorCount, warningCount, semanticsError }, + generating, + } = editorStore; if (analyzing) { return ( @@ -39,6 +42,18 @@ const GenerateButton = observer(function GenerateButton({ ); } + if (generating) { + return ( + <AnimatedButton + color="inherit" + onClick={() => editorStore.cancelModelGeneration()} + startIcon={<CloseIcon />} + > + Cancel + </AnimatedButton> + ); + } + if (semanticsError !== undefined && editorStore.opened) { return ( <AnimatedButton @@ -83,6 +98,7 @@ const GenerateButton = observer(function GenerateButton({ disabled={!editorStore.opened} color={warningCount > 0 ? 'warning' : 'primary'} startIcon={<PlayArrowIcon />} + onClick={() => editorStore.startModelGeneration()} > {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} </AnimatedButton> diff --git a/subprojects/frontend/src/editor/GeneratedModelStore.ts b/subprojects/frontend/src/editor/GeneratedModelStore.ts new file mode 100644 index 00000000..d0181eed --- /dev/null +++ b/subprojects/frontend/src/editor/GeneratedModelStore.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { makeAutoObservable } from 'mobx'; + +import GraphStore from '../graph/GraphStore'; +import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; + +export default class GeneratedModelStore { + title: string; + + message = 'Waiting for server'; + + error = false; + + graph: GraphStore | undefined; + + constructor() { + const time = new Date().toLocaleTimeString(undefined, { hour12: false }); + this.title = `Generated at ${time}`; + makeAutoObservable(this); + } + + get running(): boolean { + return !this.error && this.graph === undefined; + } + + setMessage(message: string): void { + if (this.running) { + this.message = message; + } + } + + setError(message: string): void { + if (this.running) { + this.error = true; + this.message = message; + } + } + + setSemantics(semantics: SemanticsSuccessResult): void { + if (this.running) { + this.graph = new GraphStore(); + this.graph.setSemantics(semantics); + } + } +} diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx index f8f40d22..d5801b9a 100644 --- a/subprojects/frontend/src/graph/GraphArea.tsx +++ b/subprojects/frontend/src/graph/GraphArea.tsx @@ -9,25 +9,17 @@ import { useTheme } from '@mui/material/styles'; import { observer } from 'mobx-react-lite'; import { useResizeDetector } from 'react-resize-detector'; -import Loading from '../Loading'; -import { useRootStore } from '../RootStoreProvider'; - import DotGraphVisualizer from './DotGraphVisualizer'; +import type GraphStore from './GraphStore'; import VisibilityPanel from './VisibilityPanel'; import ZoomCanvas from './ZoomCanvas'; -function GraphArea(): JSX.Element { - const { editorStore } = useRootStore(); +function GraphArea({ graph }: { graph: GraphStore }): JSX.Element { const { breakpoints } = useTheme(); const { ref, width, height } = useResizeDetector({ refreshMode: 'debounce', }); - if (editorStore === undefined) { - return <Loading />; - } - - const { graph } = editorStore; const breakpoint = breakpoints.values.sm; const dialog = width === undefined || diff --git a/subprojects/frontend/src/graph/GraphPane.tsx b/subprojects/frontend/src/graph/GraphPane.tsx index c2ef8927..67dbfcbd 100644 --- a/subprojects/frontend/src/graph/GraphPane.tsx +++ b/subprojects/frontend/src/graph/GraphPane.tsx @@ -9,9 +9,15 @@ import { Suspense, lazy } from 'react'; import Loading from '../Loading'; +import type GraphStore from './GraphStore'; + const GraphArea = lazy(() => import('./GraphArea')); -export default function GraphPane(): JSX.Element { +export default function GraphPane({ + graph, +}: { + graph: GraphStore; +}): JSX.Element { return ( <Stack direction="column" @@ -21,7 +27,7 @@ export default function GraphPane(): JSX.Element { justifyContent="center" > <Suspense fallback={<Loading />}> - <GraphArea /> + <GraphArea graph={graph} /> </Suspense> </Stack> ); diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx index e8a22e82..4b251a23 100644 --- a/subprojects/frontend/src/index.tsx +++ b/subprojects/frontend/src/index.tsx @@ -16,35 +16,101 @@ import RootStore from './RootStore'; (window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled; const initialValue = `% Metamodel -class Person { - contains Post[] posts opposite author - Person[] friend opposite friend + +abstract class CompositeElement { + contains Region[] regions +} + +class Region { + contains Vertex[] vertices opposite region +} + +abstract class Vertex { + container Region region opposite vertices + contains Transition[] outgoingTransition opposite source + Transition[] incomingTransition opposite target } -class Post { - container Person author opposite posts - Post replyTo +class Transition { + container Vertex source opposite outgoingTransition + Vertex target opposite incomingTransition } +abstract class Pseudostate extends Vertex. + +abstract class RegularState extends Vertex. + +class Entry extends Pseudostate. + +class Exit extends Pseudostate. + +class Choice extends Pseudostate. + +class FinalState extends RegularState. + +class State extends RegularState, CompositeElement. + +class Statechart extends CompositeElement. + % Constraints -error replyToNotFriend(Post x, Post y) <-> - replyTo(x, y), - author(x, xAuthor), - author(y, yAuthor), - xAuthor != yAuthor, - !friend(xAuthor, yAuthor). -error replyToCycle(Post x) <-> replyTo+(x, x). +%% Entry + +pred entryInRegion(Region r, Entry e) <-> + vertices(r, e). + +error noEntryInRegion(Region r) <-> + !entryInRegion(r, _). + +error multipleEntryInRegion(Region r) <-> + entryInRegion(r, e1), + entryInRegion(r, e2), + e1 != e2. + +error incomingToEntry(Transition t, Entry e) <-> + target(t, e). + +error noOutgoingTransitionFromEntry(Entry e) <-> + !source(_, e). + +error multipleTransitionFromEntry(Entry e, Transition t1, Transition t2) <-> + outgoingTransition(e, t1), + outgoingTransition(e, t2), + t1 != t2. + +%% Exit + +error outgoingFromExit(Transition t, Exit e) <-> + source(t, e). + +%% Final + +error outgoingFromFinal(Transition t, FinalState e) <-> + source(t, e). + +%% State vs Region + +pred stateInRegion(Region r, State s) <-> + vertices(r, s). + +error noStateInRegion(Region r) <-> + !stateInRegion(r, _). + +%% Choice + +error choiceHasNoOutgoing(Choice c) <-> + !source(_, c). + +error choiceHasNoIncoming(Choice c) <-> + !target(_, c). % Instance model -friend(a, b). -author(p1, a). -author(p2, b). -!author(Post::new, a). +Statechart(sct). % Scope -scope Post = 10..15, Person += 0. + +scope node = 20..30, Region = 2..*, Choice = 1..*, Statechart += 0. `; configure({ diff --git a/subprojects/frontend/src/table/RelationGrid.tsx b/subprojects/frontend/src/table/RelationGrid.tsx deleted file mode 100644 index 004982c9..00000000 --- a/subprojects/frontend/src/table/RelationGrid.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import Box from '@mui/material/Box'; -import { - DataGrid, - type GridRenderCellParams, - type GridColDef, -} from '@mui/x-data-grid'; -import { observer } from 'mobx-react-lite'; -import { useMemo } from 'react'; - -import type GraphStore from '../graph/GraphStore'; - -import TableToolbar from './TableToolbar'; -import ValueRenderer from './ValueRenderer'; - -interface Row { - nodes: string[]; - value: string; -} - -function RelationGrid({ graph }: { graph: GraphStore }): JSX.Element { - const { - selectedSymbol, - semantics: { nodes, partialInterpretation }, - } = graph; - const symbolName = selectedSymbol?.name; - const arity = selectedSymbol?.arity ?? 0; - - const columns = useMemo<GridColDef<Row>[]>(() => { - const defs: GridColDef<Row>[] = []; - for (let i = 0; i < arity; i += 1) { - defs.push({ - field: `n${i}`, - headerName: String(i + 1), - valueGetter: (row) => row.row.nodes[i] ?? '', - flex: 1, - }); - } - defs.push({ - field: 'value', - headerName: 'Value', - flex: 1, - renderCell: ({ value }: GridRenderCellParams<Row, string>) => ( - <ValueRenderer value={value} /> - ), - }); - return defs; - }, [arity]); - - const rows = useMemo<Row[]>(() => { - if (symbolName === undefined) { - return []; - } - const interpretation = partialInterpretation[symbolName] ?? []; - return interpretation.map((tuple) => { - const nodeNames: string[] = []; - for (let i = 0; i < arity; i += 1) { - const index = tuple[i]; - if (typeof index === 'number') { - const node = nodes[index]; - if (node !== undefined) { - nodeNames.push(node.name); - } - } - } - return { - nodes: nodeNames, - value: String(tuple[arity]), - }; - }); - }, [arity, nodes, partialInterpretation, symbolName]); - - return ( - <Box - width="100%" - height="100%" - p={1} - sx={(theme) => ({ - '.MuiDataGrid-withBorderColor': { - borderColor: - theme.palette.mode === 'dark' - ? theme.palette.divider - : theme.palette.outer.border, - }, - })} - > - <DataGrid - slots={{ toolbar: TableToolbar }} - slotProps={{ - toolbar: { - graph, - }, - }} - density="compact" - rowSelection={false} - columns={columns} - rows={rows} - getRowId={(row) => row.nodes.join(',')} - /> - </Box> - ); -} - -export default observer(RelationGrid); diff --git a/subprojects/frontend/src/table/TableArea.tsx b/subprojects/frontend/src/table/TableArea.tsx index cf37b96a..166b8adf 100644 --- a/subprojects/frontend/src/table/TableArea.tsx +++ b/subprojects/frontend/src/table/TableArea.tsx @@ -4,21 +4,106 @@ * SPDX-License-Identifier: EPL-2.0 */ +import Box from '@mui/material/Box'; +import { + DataGrid, + type GridRenderCellParams, + type GridColDef, +} from '@mui/x-data-grid'; import { observer } from 'mobx-react-lite'; +import { useMemo } from 'react'; -import Loading from '../Loading'; -import { useRootStore } from '../RootStoreProvider'; +import type GraphStore from '../graph/GraphStore'; -import RelationGrid from './RelationGrid'; +import TableToolbar from './TableToolbar'; +import ValueRenderer from './ValueRenderer'; -function TablePane(): JSX.Element { - const { editorStore } = useRootStore(); +interface Row { + nodes: string[]; + value: string; +} + +function TableArea({ graph }: { graph: GraphStore }): JSX.Element { + const { + selectedSymbol, + semantics: { nodes, partialInterpretation }, + } = graph; + const symbolName = selectedSymbol?.name; + const arity = selectedSymbol?.arity ?? 0; + + const columns = useMemo<GridColDef<Row>[]>(() => { + const defs: GridColDef<Row>[] = []; + for (let i = 0; i < arity; i += 1) { + defs.push({ + field: `n${i}`, + headerName: String(i + 1), + valueGetter: (row) => row.row.nodes[i] ?? '', + flex: 1, + }); + } + defs.push({ + field: 'value', + headerName: 'Value', + flex: 1, + renderCell: ({ value }: GridRenderCellParams<Row, string>) => ( + <ValueRenderer value={value} /> + ), + }); + return defs; + }, [arity]); - if (editorStore === undefined) { - return <Loading />; - } + const rows = useMemo<Row[]>(() => { + if (symbolName === undefined) { + return []; + } + const interpretation = partialInterpretation[symbolName] ?? []; + return interpretation.map((tuple) => { + const nodeNames: string[] = []; + for (let i = 0; i < arity; i += 1) { + const index = tuple[i]; + if (typeof index === 'number') { + const node = nodes[index]; + if (node !== undefined) { + nodeNames.push(node.name); + } + } + } + return { + nodes: nodeNames, + value: String(tuple[arity]), + }; + }); + }, [arity, nodes, partialInterpretation, symbolName]); - return <RelationGrid graph={editorStore.graph} />; + return ( + <Box + width="100%" + height="100%" + p={1} + sx={(theme) => ({ + '.MuiDataGrid-withBorderColor': { + borderColor: + theme.palette.mode === 'dark' + ? theme.palette.divider + : theme.palette.outer.border, + }, + })} + > + <DataGrid + slots={{ toolbar: TableToolbar }} + slotProps={{ + toolbar: { + graph, + }, + }} + density="compact" + rowSelection={false} + columns={columns} + rows={rows} + getRowId={(row) => row.nodes.join(',')} + /> + </Box> + ); } -export default observer(TablePane); +export default observer(TableArea); diff --git a/subprojects/frontend/src/table/TablePane.tsx b/subprojects/frontend/src/table/TablePane.tsx index 01442c3a..8b640217 100644 --- a/subprojects/frontend/src/table/TablePane.tsx +++ b/subprojects/frontend/src/table/TablePane.tsx @@ -8,14 +8,19 @@ import Stack from '@mui/material/Stack'; import { Suspense, lazy } from 'react'; import Loading from '../Loading'; +import type GraphStore from '../graph/GraphStore'; const TableArea = lazy(() => import('./TableArea')); -export default function TablePane(): JSX.Element { +export default function TablePane({ + graph, +}: { + graph: GraphStore; +}): JSX.Element { return ( <Stack direction="column" height="100%" overflow="auto" alignItems="center"> <Suspense fallback={<Loading />}> - <TableArea /> + <TableArea graph={graph} /> </Suspense> </Stack> ); diff --git a/subprojects/frontend/src/xtext/ModelGenerationService.ts b/subprojects/frontend/src/xtext/ModelGenerationService.ts new file mode 100644 index 00000000..1e9f837a --- /dev/null +++ b/subprojects/frontend/src/xtext/ModelGenerationService.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type EditorStore from '../editor/EditorStore'; + +import type UpdateService from './UpdateService'; +import { ModelGenerationResult } from './xtextServiceResults'; + +export default class ModelGenerationService { + constructor( + private readonly store: EditorStore, + private readonly updateService: UpdateService, + ) {} + + onPush(push: unknown): void { + const result = ModelGenerationResult.parse(push); + if ('status' in result) { + this.store.setGeneratedModelMessage(result.uuid, result.status); + } else if ('error' in result) { + this.store.setGeneratedModelError(result.uuid, result.error); + } else { + this.store.setGeneratedModelSemantics(result.uuid, result); + } + } + + onDisconnect(): void { + this.store.modelGenerationCancelled(); + } + + async start(): Promise<void> { + const result = await this.updateService.startModelGeneration(); + if (!result.cancelled) { + this.store.addGeneratedModel(result.data.uuid); + } + } + + async cancel(): Promise<void> { + const result = await this.updateService.cancelModelGeneration(); + if (!result.cancelled) { + this.store.modelGenerationCancelled(); + } + } +} diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index 1ac722e1..d1246d5e 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts @@ -22,6 +22,7 @@ import { FormattingResult, isConflictResult, OccurrencesResult, + ModelGenerationStartedResult, } from './xtextServiceResults'; const UPDATE_TIMEOUT_MS = 500; @@ -341,4 +342,42 @@ export default class UpdateService { } return { cancelled: false, data: parsedOccurrencesResult }; } + + async startModelGeneration(): Promise< + CancellableResult<ModelGenerationStartedResult> + > { + try { + await this.updateOrThrow(); + } catch (error) { + if (error instanceof CancelledError || error instanceof TimeoutError) { + return { cancelled: true }; + } + throw error; + } + log.debug('Starting model generation'); + const data = await this.webSocketClient.send({ + resource: this.resourceName, + serviceType: 'modelGeneration', + requiredStateId: this.xtextStateId, + start: true, + }); + if (isConflictResult(data)) { + return { cancelled: true }; + } + const parsedResult = ModelGenerationStartedResult.parse(data); + return { cancelled: false, data: parsedResult }; + } + + async cancelModelGeneration(): Promise<CancellableResult<unknown>> { + log.debug('Cancelling model generation'); + const data = await this.webSocketClient.send({ + resource: this.resourceName, + serviceType: 'modelGeneration', + cancel: true, + }); + if (isConflictResult(data)) { + return { cancelled: true }; + } + return { cancelled: false, data }; + } } diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index 87778084..77980d35 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts @@ -16,6 +16,7 @@ import getLogger from '../utils/getLogger'; import ContentAssistService from './ContentAssistService'; import HighlightingService from './HighlightingService'; +import ModelGenerationService from './ModelGenerationService'; import OccurrencesService from './OccurrencesService'; import SemanticsService from './SemanticsService'; import UpdateService from './UpdateService'; @@ -40,6 +41,8 @@ export default class XtextClient { private readonly semanticsService: SemanticsService; + private readonly modelGenerationService: ModelGenerationService; + constructor( private readonly store: EditorStore, private readonly pwaStore: PWAStore, @@ -58,6 +61,10 @@ export default class XtextClient { this.validationService = new ValidationService(store, this.updateService); this.occurrencesService = new OccurrencesService(store, this.updateService); this.semanticsService = new SemanticsService(store, this.validationService); + this.modelGenerationService = new ModelGenerationService( + store, + this.updateService, + ); } start(): void { @@ -75,6 +82,7 @@ export default class XtextClient { this.highlightingService.onDisconnect(); this.validationService.onDisconnect(); this.occurrencesService.onDisconnect(); + this.modelGenerationService.onDisconnect(); } onTransaction(transaction: Transaction): void { @@ -101,7 +109,7 @@ export default class XtextClient { ); return; } - if (stateId !== xtextStateId) { + if (stateId !== xtextStateId && service !== 'modelGeneration') { log.error( 'Unexpected xtext state id: expected:', xtextStateId, @@ -122,6 +130,9 @@ export default class XtextClient { case 'semantics': this.semanticsService.onPush(push); return; + case 'modelGeneration': + this.modelGenerationService.onPush(push); + return; default: throw new Error('Unknown service'); } @@ -131,6 +142,14 @@ export default class XtextClient { return this.contentAssistService.contentAssist(context); } + startModelGeneration(): Promise<void> { + return this.modelGenerationService.start(); + } + + cancelModelGeneration(): Promise<void> { + return this.modelGenerationService.cancel(); + } + formatText(): void { this.updateService.formatText().catch((e) => { log.error('Error while formatting text', e); diff --git a/subprojects/frontend/src/xtext/xtextMessages.ts b/subprojects/frontend/src/xtext/xtextMessages.ts index 971720e1..15831c5a 100644 --- a/subprojects/frontend/src/xtext/xtextMessages.ts +++ b/subprojects/frontend/src/xtext/xtextMessages.ts @@ -38,6 +38,7 @@ export const XtextWebPushService = z.enum([ 'highlight', 'validate', 'semantics', + 'modelGeneration', ]); export type XtextWebPushService = z.infer<typeof XtextWebPushService>; diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index caf2cf0b..e473bd48 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts @@ -126,6 +126,14 @@ export const FormattingResult = DocumentStateResult.extend({ export type FormattingResult = z.infer<typeof FormattingResult>; +export const ModelGenerationStartedResult = z.object({ + uuid: z.string().nonempty(), +}); + +export type ModelGenerationStartedResult = z.infer< + typeof ModelGenerationStartedResult +>; + export const NodeMetadata = z.object({ name: z.string(), simpleName: z.string(), @@ -171,3 +179,19 @@ export const SemanticsResult = z.union([ ]); export type SemanticsResult = z.infer<typeof SemanticsResult>; + +export const ModelGenerationResult = z.union([ + z.object({ + uuid: z.string().nonempty(), + status: z.string(), + }), + z.object({ + uuid: z.string().nonempty(), + error: z.string(), + }), + SemanticsSuccessResult.extend({ + uuid: z.string().nonempty(), + }), +]); + +export type ModelGenerationResult = z.infer<typeof ModelGenerationResult>; diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java index 0c18b1b3..d6115c5c 100644 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java @@ -47,12 +47,22 @@ public class MetadataCreator { } public List<NodeMetadata> getNodesMetadata() { - var nodes = new NodeMetadata[initializer.getNodeCount()]; + return getNodesMetadata(initializer.getNodeCount()); + } + + public List<NodeMetadata> getNodesMetadata(int nodeCount) { + var nodes = new NodeMetadata[Math.max(initializer.getNodeCount(), nodeCount)]; for (var entry : initializer.getNodeTrace().keyValuesView()) { var node = entry.getOne(); var id = entry.getTwo(); nodes[id] = getNodeMetadata(node); } + for (int i = 0; i < nodes.length; i++) { + if (nodes[i] == null) { + var nodeName = "#" + i; + nodes[i] = new NodeMetadata(nodeName, nodeName, NodeKind.IMPLICIT); + } + } return List.of(nodes); } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java new file mode 100644 index 00000000..fc06fd2e --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +import org.eclipse.xtext.web.server.IServiceResult; + +public record ModelGenerationCancelledResult() implements IServiceResult { +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java new file mode 100644 index 00000000..bedaeb35 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +import java.util.UUID; + +public record ModelGenerationErrorResult(UUID uuid, String error) implements ModelGenerationResult { +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java new file mode 100644 index 00000000..b0a1912c --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +import org.eclipse.xtext.util.CancelIndicator; + +public class ModelGenerationManager { + private final Object lockObject = new Object(); + private ModelGenerationWorker worker; + private boolean disposed; + + boolean setActiveModelGenerationWorker(ModelGenerationWorker worker, CancelIndicator cancelIndicator) { + synchronized (lockObject) { + cancel(); + if (disposed || cancelIndicator.isCanceled()) { + return true; + } + this.worker = worker; + } + return false; + } + + public void cancel() { + synchronized (lockObject) { + if (worker != null) { + worker.cancel(); + worker = null; + } + } + } + + public void dispose() { + synchronized (lockObject) { + disposed = true; + cancel(); + } + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java new file mode 100644 index 00000000..cf06f447 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +import org.eclipse.xtext.web.server.IServiceResult; + +import java.util.UUID; + +public sealed interface ModelGenerationResult extends IServiceResult permits ModelGenerationSuccessResult, + ModelGenerationErrorResult, ModelGenerationStatusResult { + UUID uuid(); +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java new file mode 100644 index 00000000..5a60007f --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +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.util.concurrent.CancelableUnitOfWork; +import org.eclipse.xtext.web.server.model.IXtextWebDocument; +import tools.refinery.language.web.semantics.SemanticsService; +import tools.refinery.language.web.xtext.server.push.PushWebDocument; +import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; + +@Singleton +public class ModelGenerationService { + public static final String SERVICE_NAME = "modelGeneration"; + public static final String MODEL_GENERATION_EXECUTOR = "modelGeneration"; + public static final String MODEL_GENERATION_TIMEOUT_EXECUTOR = "modelGenerationTimeout"; + + @Inject + private OperationCanceledManager operationCanceledManager; + + @Inject + private Provider<ModelGenerationWorker> workerProvider; + + private final long timeoutSec; + + public ModelGenerationService() { + timeoutSec = SemanticsService.getTimeout("REFINERY_MODEL_GENERATION_TIMEOUT_SEC").orElse(600L); + } + + public ModelGenerationStartedResult generateModel(PushWebDocumentAccess document){ + return document.modify(new CancelableUnitOfWork<>() { + @Override + public ModelGenerationStartedResult exec(IXtextWebDocument state, CancelIndicator cancelIndicator) { + var pushState = (PushWebDocument) state; + var worker = workerProvider.get(); + worker.setState(pushState, timeoutSec); + var manager = pushState.getModelGenerationManager(); + worker.start(); + boolean canceled = manager.setActiveModelGenerationWorker(worker, cancelIndicator); + if (canceled) { + worker.cancel(); + operationCanceledManager.throwOperationCanceledException(); + } + return new ModelGenerationStartedResult(worker.getUuid()); + } + }); + } + + public ModelGenerationCancelledResult cancelModelGeneration(PushWebDocumentAccess document) { + document.cancelModelGeneration(); + return new ModelGenerationCancelledResult(); + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java new file mode 100644 index 00000000..8c0e73c7 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +import org.eclipse.xtext.web.server.IServiceResult; + +import java.util.UUID; + +public record ModelGenerationStartedResult(UUID uuid) implements IServiceResult { +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java new file mode 100644 index 00000000..a6589870 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +import java.util.UUID; + +public record ModelGenerationStatusResult(UUID uuid, String status) implements ModelGenerationResult { +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java new file mode 100644 index 00000000..21be4e08 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +import com.google.gson.JsonObject; +import tools.refinery.language.semantics.metadata.NodeMetadata; +import tools.refinery.language.semantics.metadata.RelationMetadata; + +import java.util.List; +import java.util.UUID; + +public record ModelGenerationSuccessResult(UUID uuid, List<NodeMetadata> nodes, List<RelationMetadata> relations, + JsonObject partialInterpretation) implements ModelGenerationResult { +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java new file mode 100644 index 00000000..1f430da6 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java @@ -0,0 +1,228 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.generator; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.diagnostics.Severity; +import org.eclipse.xtext.resource.IResourceFactory; +import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.service.OperationCanceledManager; +import org.eclipse.xtext.util.LazyStringInputStream; +import org.eclipse.xtext.validation.CheckMode; +import org.eclipse.xtext.validation.IResourceValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.web.semantics.PartialInterpretation2Json; +import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider; +import tools.refinery.language.web.xtext.server.push.PushWebDocument; +import tools.refinery.store.dse.propagation.PropagationAdapter; +import tools.refinery.store.dse.strategy.BestFirstStoreManager; +import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter; +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.statecoding.StateCoderAdapter; +import tools.refinery.store.util.CancellationToken; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.*; + +public class ModelGenerationWorker implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(ModelGenerationWorker.class); + + private final UUID uuid = UUID.randomUUID(); + + private PushWebDocument state; + + private String text; + + private volatile boolean timedOut; + + private volatile boolean cancelled; + + @Inject + private OperationCanceledManager operationCanceledManager; + + @Inject + private Provider<XtextResourceSet> resourceSetProvider; + + @Inject + private IResourceFactory resourceFactory; + + @Inject + private IResourceValidator resourceValidator; + + @Inject + private ModelInitializer initializer; + + @Inject + private MetadataCreator metadataCreator; + + @Inject + private PartialInterpretation2Json partialInterpretation2Json; + + private final Object lockObject = new Object(); + + private ExecutorService executorService; + + private ScheduledExecutorService scheduledExecutorService; + + private long timeoutSec; + + private Future<?> future; + + private ScheduledFuture<?> timeoutFuture; + + private final CancellationToken cancellationToken = () -> { + if (cancelled || Thread.interrupted()) { + operationCanceledManager.throwOperationCanceledException(); + } + }; + + @Inject + public void setExecutorServiceProvider(ThreadPoolExecutorServiceProvider provider) { + executorService = provider.get(ModelGenerationService.MODEL_GENERATION_EXECUTOR); + scheduledExecutorService = provider.getScheduled(ModelGenerationService.MODEL_GENERATION_TIMEOUT_EXECUTOR); + } + + public void setState(PushWebDocument state, long timeoutSec) { + this.state = state; + this.timeoutSec = timeoutSec; + text = state.getText(); + } + + public UUID getUuid() { + return uuid; + } + + public void start() { + synchronized (lockObject) { + LOG.debug("Enqueueing model generation: {}", uuid); + future = executorService.submit(this); + } + } + + public void startTimeout() { + synchronized (lockObject) { + LOG.debug("Starting model generation: {}", uuid); + cancellationToken.checkCancelled(); + timeoutFuture = scheduledExecutorService.schedule(() -> cancel(true), timeoutSec, TimeUnit.SECONDS); + } + } + + // We catch {@code Throwable} to handle {@code OperationCancelledError}, but we rethrow fatal JVM errors. + @SuppressWarnings("squid:S1181") + @Override + public void run() { + startTimeout(); + notifyResult(new ModelGenerationStatusResult(uuid, "Initializing model generator")); + ModelGenerationResult result; + try { + result = doRun(); + } catch (Throwable e) { + if (operationCanceledManager.isOperationCanceledException(e)) { + var message = timedOut ? "Model generation timed out" : "Model generation cancelled"; + LOG.debug("{}: {}", message, uuid); + notifyResult(new ModelGenerationErrorResult(uuid, message)); + } else if (e instanceof Error error) { + // Make sure we don't try to recover from any fatal JVM errors. + throw error; + } else { + LOG.debug("Model generation error", e); + notifyResult(new ModelGenerationErrorResult(uuid, e.toString())); + } + return; + } + notifyResult(result); + } + + private void notifyResult(ModelGenerationResult result) { + state.notifyPrecomputationListeners(ModelGenerationService.SERVICE_NAME, result); + } + + public ModelGenerationResult doRun() throws IOException { + cancellationToken.checkCancelled(); + var resourceSet = resourceSetProvider.get(); + var uri = URI.createURI("__synthetic_" + uuid + ".problem"); + var resource = resourceFactory.createResource(uri); + resourceSet.getResources().add(resource); + var inputStream = new LazyStringInputStream(text); + resource.load(inputStream, Map.of()); + cancellationToken.checkCancelled(); + var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> cancelled || Thread.interrupted()); + cancellationToken.checkCancelled(); + for (var issue : issues) { + if (issue.getSeverity() == Severity.ERROR) { + return new ModelGenerationErrorResult(uuid, "Validation error: " + issue.getMessage()); + } + } + if (resource.getContents().isEmpty() || !(resource.getContents().get(0) instanceof Problem problem)) { + return new ModelGenerationErrorResult(uuid, "Model generation problem not found"); + } + cancellationToken.checkCancelled(); + var storeBuilder = ModelStore.builder() + .cancellationToken(cancellationToken) + .with(ViatraModelQueryAdapter.builder()) + .with(PropagationAdapter.builder()) + .with(StateCoderAdapter.builder()) + .with(DesignSpaceExplorationAdapter.builder()) + .with(ReasoningAdapter.builder()); + var modelSeed = initializer.createModel(problem, storeBuilder); + var store = storeBuilder.build(); + cancellationToken.checkCancelled(); + var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); + var initialVersion = model.commit(); + cancellationToken.checkCancelled(); + notifyResult(new ModelGenerationStatusResult(uuid, "Generating model")); + var bestFirst = new BestFirstStoreManager(store, 1); + bestFirst.startExploration(initialVersion); + cancellationToken.checkCancelled(); + var solutionStore = bestFirst.getSolutionStore(); + if (solutionStore.getSolutions().isEmpty()) { + return new ModelGenerationErrorResult(uuid, "Problem is unsatisfiable"); + } + notifyResult(new ModelGenerationStatusResult(uuid, "Saving generated model")); + model.restore(solutionStore.getSolutions().get(0).version()); + cancellationToken.checkCancelled(); + metadataCreator.setInitializer(initializer); + var nodesMetadata = metadataCreator.getNodesMetadata(model.getAdapter(ReasoningAdapter.class).getNodeCount()); + cancellationToken.checkCancelled(); + var relationsMetadata = metadataCreator.getRelationsMetadata(); + cancellationToken.checkCancelled(); + var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model, + Concreteness.CANDIDATE, cancellationToken); + return new ModelGenerationSuccessResult(uuid, nodesMetadata, relationsMetadata, partialInterpretation); + } + + public void cancel() { + cancel(false); + } + + public void cancel(boolean timedOut) { + synchronized (lockObject) { + LOG.trace("Cancelling model generation: {}", uuid); + this.timedOut = timedOut; + cancelled = true; + if (future != null) { + future.cancel(true); + future = null; + } + if (timeoutFuture != null) { + timeoutFuture.cancel(true); + timeoutFuture = null; + } + } + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java new file mode 100644 index 00000000..5d5da8fe --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> + * + * 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.Singleton; +import tools.refinery.language.semantics.model.ModelInitializer; +import tools.refinery.language.semantics.model.SemanticsUtils; +import tools.refinery.store.map.Cursor; +import tools.refinery.store.model.Model; +import tools.refinery.store.reasoning.ReasoningAdapter; +import tools.refinery.store.reasoning.literal.Concreteness; +import tools.refinery.store.reasoning.representation.PartialRelation; +import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; +import tools.refinery.store.tuple.Tuple; +import tools.refinery.store.util.CancellationToken; + +import java.util.TreeMap; + +@Singleton +public class PartialInterpretation2Json { + @Inject + private SemanticsUtils semanticsUtils; + + public JsonObject getPartialInterpretation(ModelInitializer initializer, Model model, Concreteness concreteness, + CancellationToken cancellationToken) { + 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, concreteness, partialSymbol); + var name = semanticsUtils.getName(relation).orElse(partialSymbol.name()); + json.add(name, tuples); + cancellationToken.checkCancelled(); + } + json.add("builtin::count", getCountJson(model)); + return json; + } + + private static JsonArray getTuplesJson(ReasoningAdapter adapter, Concreteness concreteness, + PartialRelation partialSymbol) { + var interpretation = adapter.getPartialInterpretation(concreteness, partialSymbol); + var cursor = interpretation.getAll(); + return getTuplesJson(cursor); + } + + private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) { + var map = new TreeMap<Tuple, Object>(); + 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, Object 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; + } + + private static JsonArray getCountJson(Model model) { + var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE); + var cursor = interpretation.getAll(); + return getTuplesJson(cursor); + + } +} diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java index 26924f0a..331ef84b 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java @@ -54,7 +54,7 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { warmupTimeoutMs = getTimeout("REFINERY_SEMANTICS_WARMUP_TIMEOUT_MS").orElse(timeoutMs * 2); } - private static Optional<Long> getTimeout(String name) { + public static Optional<Long> getTimeout(String name) { return Optional.ofNullable(System.getenv(name)).map(Long::parseUnsignedLong); } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java index 33b1c4fb..512c2778 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java @@ -5,8 +5,6 @@ */ package tools.refinery.language.web.semantics; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import com.google.inject.Inject; import org.eclipse.emf.common.util.Diagnostic; import org.eclipse.emf.ecore.EObject; @@ -20,31 +18,24 @@ 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; import tools.refinery.store.dse.propagation.PropagationAdapter; -import tools.refinery.store.map.Cursor; -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.reasoning.translator.TranslationException; -import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; -import tools.refinery.store.tuple.Tuple; import tools.refinery.store.util.CancellationToken; import java.util.ArrayList; -import java.util.TreeMap; import java.util.concurrent.Callable; class SemanticsWorker implements Callable<SemanticsResult> { private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError"; @Inject - private SemanticsUtils semanticsUtils; + private PartialInterpretation2Json partialInterpretation2Json; @Inject private OperationCanceledManager operationCanceledManager; @@ -93,7 +84,8 @@ class SemanticsWorker implements Callable<SemanticsResult> { cancellationToken.checkCancelled(); var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); cancellationToken.checkCancelled(); - var partialInterpretation = getPartialInterpretation(initializer, model); + var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model, + Concreteness.PARTIAL, cancellationToken); return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation); } catch (TracedException e) { @@ -104,55 +96,6 @@ class SemanticsWorker implements Callable<SemanticsResult> { } } - private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model) { - 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); - cancellationToken.checkCancelled(); - } - json.add("builtin::count", getCountJson(model)); - return json; - } - - private static JsonArray getTuplesJson(ReasoningAdapter adapter, PartialRelation partialSymbol) { - var interpretation = adapter.getPartialInterpretation(Concreteness.PARTIAL, partialSymbol); - var cursor = interpretation.getAll(); - return getTuplesJson(cursor); - } - - private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) { - var map = new TreeMap<Tuple, Object>(); - 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, Object 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; - } - - private static JsonArray getCountJson(Model model) { - var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE); - var cursor = interpretation.getAll(); - return getTuplesJson(cursor); - } - private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) { if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) { return new SemanticsInternalErrorResult(message); diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java index ba26ff58..625909b9 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java @@ -13,9 +13,13 @@ import tools.refinery.language.web.semantics.SemanticsService; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; @@ -24,6 +28,8 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { private static final String DOCUMENT_LOCK_EXECUTOR; private static final AtomicInteger POOL_ID = new AtomicInteger(1); + private final Map<String, ScheduledExecutorService> scheduledInstanceCache = + Collections.synchronizedMap(new HashMap<>()); private final int executorThreadCount; private final int lockExecutorThreadCount; private final int semanticsExecutorThreadCount; @@ -58,11 +64,15 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt); } + public ScheduledExecutorService getScheduled(String key) { + return scheduledInstanceCache.computeIfAbsent(key, this::createScheduledInstance); + } + @Override protected ExecutorService createInstance(String key) { String name = "xtext-" + POOL_ID.getAndIncrement(); if (key != null) { - name = name + key + "-"; + name = name + "-" + key; } var threadFactory = new Factory(name, 5); int size = getSize(key); @@ -72,6 +82,15 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { return Executors.newFixedThreadPool(size, threadFactory); } + protected ScheduledExecutorService createScheduledInstance(String key) { + String name = "xtext-scheduled-" + POOL_ID.getAndIncrement(); + if (key != null) { + name = name + "-" + key; + } + var threadFactory = new Factory(name, 5); + return Executors.newScheduledThreadPool(1, threadFactory); + } + private int getSize(String key) { if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) { return semanticsExecutorThreadCount; @@ -82,6 +101,17 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { } } + @Override + public void dispose() { + super.dispose(); + synchronized (scheduledInstanceCache) { + for (var instance : scheduledInstanceCache.values()) { + instance.shutdown(); + } + scheduledInstanceCache.clear(); + } + } + private static class Factory implements ThreadFactory { // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}. @SuppressWarnings("squid:S3014") diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java index 74456604..a3792bac 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java @@ -187,7 +187,7 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener var document = subscription.get(); if (document != null) { document.removePrecomputationListener(this); - document.cancelBackgroundWork(); + document.dispose(); } } } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java index d4a8c433..a04ee226 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java @@ -5,14 +5,17 @@ */ package tools.refinery.language.web.xtext.server.push; +import com.google.common.base.Optional; import com.google.inject.Inject; import org.eclipse.xtext.web.server.IServiceContext; +import org.eclipse.xtext.web.server.InvalidRequestException; 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.generator.ModelGenerationService; import tools.refinery.language.web.semantics.SemanticsService; import tools.refinery.language.web.xtext.server.SubscribingServiceContext; @@ -21,6 +24,9 @@ public class PushServiceDispatcher extends XtextServiceDispatcher { @Inject private SemanticsService semanticsService; + @Inject + private ModelGenerationService modelGenerationService; + @Override @Inject protected void registerPreComputedServices(PrecomputedServiceRegistry registry) { @@ -37,4 +43,37 @@ public class PushServiceDispatcher extends XtextServiceDispatcher { } return document; } + + @Override + protected ServiceDescriptor createServiceDescriptor(String serviceType, IServiceContext context) { + if (ModelGenerationService.SERVICE_NAME.equals(serviceType)) { + return getModelGenerationService(context); + } + return super.createServiceDescriptor(serviceType, context); + } + + protected ServiceDescriptor getModelGenerationService(IServiceContext context) throws InvalidRequestException { + var document = (PushWebDocumentAccess) getDocumentAccess(context); + // Using legacy Guava methods because of the Xtext dependency. + @SuppressWarnings({"Guava", "squid:S4738"}) + boolean start = getBoolean(context, "start", Optional.of(false)); + @SuppressWarnings({"Guava", "squid:S4738"}) + boolean cancel = getBoolean(context, "cancel", Optional.of(false)); + if (!start && !cancel) { + throw new InvalidRequestException("Either start of cancel must be specified"); + } + var descriptor = new ServiceDescriptor(); + descriptor.setService(() -> { + try { + if (start) { + return modelGenerationService.generateModel(document); + } else { + return modelGenerationService.cancelModelGeneration(document); + } + } catch (RuntimeException e) { + return handleError(descriptor, e); + } + }); + return descriptor; + } } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java index 2d43fb26..ca97147a 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java @@ -13,19 +13,18 @@ import org.eclipse.xtext.web.server.model.DocumentSynchronizer; import org.eclipse.xtext.web.server.model.XtextWebDocument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.refinery.language.web.generator.ModelGenerationManager; 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); private final List<PrecomputationListener> precomputationListeners = new ArrayList<>(); - private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>(); + private final ModelGenerationManager modelGenerationManager = new ModelGenerationManager(); private final DocumentSynchronizer synchronizer; @@ -34,6 +33,10 @@ public class PushWebDocument extends XtextWebDocument { this.synchronizer = synchronizer; } + public ModelGenerationManager getModelGenerationManager() { + return modelGenerationManager; + } + public void addPrecomputationListener(PrecomputationListener listener) { synchronized (precomputationListeners) { if (precomputationListeners.contains(listener)) { @@ -52,15 +55,13 @@ public class PushWebDocument extends XtextWebDocument { public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, CancelIndicator cancelIndicator, boolean logCacheMiss) { - var serviceClass = service.getClass(); var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); - precomputedServices.put(serviceClass, result); if (result != null) { notifyPrecomputationListeners(serviceName, result); } } - private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { + public <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { var resourceId = getResourceId(); if (resourceId == null) { return; @@ -86,7 +87,12 @@ public class PushWebDocument extends XtextWebDocument { } } - public void cancelBackgroundWork() { + public void cancelModelGeneration() { + modelGenerationManager.cancel(); + } + + public void dispose() { synchronizer.setCanceled(true); + modelGenerationManager.dispose(); } } diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java index c72e8e67..1e68b244 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java @@ -74,4 +74,8 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess { } throw new IllegalArgumentException("Unknown precomputed service: " + service); } + + public void cancelModelGeneration() { + pushDocument.cancelModelGeneration(); + } } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java index 1dda7ac1..7f0ef8b4 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java @@ -47,6 +47,8 @@ public interface ReasoningAdapter extends ModelAdapter { boolean cleanup(int nodeToDelete); + int getNodeCount(); + static ReasoningBuilder builder() { return new ReasoningBuilderImpl(); } diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java index f91fdd07..bd16bdfa 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java @@ -204,4 +204,10 @@ class ReasoningAdapterImpl implements ReasoningAdapter { } return true; } + + @Override + public int getNodeCount() { + Integer nodeCount = nodeCountInterpretation.get(Tuple.of()); + return nodeCount == null ? 0 : nodeCount; + } } -- cgit v1.2.3-54-g00ecf From 5999eb4433dc5a758674d9c941d97bbaf48d030d Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Wed, 13 Sep 2023 19:04:39 +0200 Subject: fix: hide new node names in generated models --- .../language/semantics/metadata/MetadataCreator.java | 19 ++++++++++++++----- .../language/web/generator/ModelGenerationWorker.java | 3 ++- 2 files changed, 16 insertions(+), 6 deletions(-) (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java index b6a2cdf0..cc262129 100644 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java @@ -46,27 +46,36 @@ public class MetadataCreator { relationScope = scopeProvider.getScope(problem, ProblemPackage.Literals.ASSERTION__RELATION); } + public static String unnamedNode(int nodeId) { + return "::" + nodeId; + } + public List<NodeMetadata> getNodesMetadata() { - return getNodesMetadata(initializer.getNodeCount()); + return getNodesMetadata(initializer.getNodeCount(), true); } - public List<NodeMetadata> getNodesMetadata(int nodeCount) { + public List<NodeMetadata> getNodesMetadata(int nodeCount, boolean preserveNewNodes) { var nodes = new NodeMetadata[Math.max(initializer.getNodeCount(), nodeCount)]; for (var entry : initializer.getNodeTrace().keyValuesView()) { var node = entry.getOne(); var id = entry.getTwo(); - nodes[id] = getNodeMetadata(node); + nodes[id] = getNodeMetadata(id, node, preserveNewNodes); } for (int i = 0; i < nodes.length; i++) { if (nodes[i] == null) { - var nodeName = "::" + i; + var nodeName = unnamedNode(i); nodes[i] = new NodeMetadata(nodeName, nodeName, NodeKind.IMPLICIT); } } return List.of(nodes); } - private NodeMetadata getNodeMetadata(Node node) { + private NodeMetadata getNodeMetadata(int nodeId, Node node, boolean preserveNewNodes) { + var kind = getNodeKind(node); + if (!preserveNewNodes && kind == NodeKind.NEW) { + var nodeName = unnamedNode(nodeId); + return new NodeMetadata(nodeName, nodeName, NodeKind.IMPLICIT); + } var qualifiedName = getQualifiedName(node); var simpleName = getSimpleName(node, qualifiedName, nodeScope); return new NodeMetadata(qualifiedNameConverter.toString(qualifiedName), diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java index 1f430da6..e14982a7 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java @@ -197,7 +197,8 @@ public class ModelGenerationWorker implements Runnable { model.restore(solutionStore.getSolutions().get(0).version()); cancellationToken.checkCancelled(); metadataCreator.setInitializer(initializer); - var nodesMetadata = metadataCreator.getNodesMetadata(model.getAdapter(ReasoningAdapter.class).getNodeCount()); + var nodesMetadata = metadataCreator.getNodesMetadata(model.getAdapter(ReasoningAdapter.class).getNodeCount(), + false); cancellationToken.checkCancelled(); var relationsMetadata = metadataCreator.getRelationsMetadata(); cancellationToken.checkCancelled(); -- cgit v1.2.3-54-g00ecf From e0efa34eee39f3edf7ee95cbb4ae209477ed8206 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Wed, 13 Sep 2023 19:07:16 +0200 Subject: refactor: do not construct uneeded interpretation Model generation only has to extract the candidate interpretaion, partial intepretation queries are only needed if they appear in a rule, constraint, or objective. --- .../tools/refinery/language/web/generator/ModelGenerationWorker.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java index e14982a7..77fc7484 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java @@ -178,7 +178,8 @@ public class ModelGenerationWorker implements Runnable { .with(PropagationAdapter.builder()) .with(StateCoderAdapter.builder()) .with(DesignSpaceExplorationAdapter.builder()) - .with(ReasoningAdapter.builder()); + .with(ReasoningAdapter.builder() + .requiredInterpretations(Concreteness.CANDIDATE)); var modelSeed = initializer.createModel(problem, storeBuilder); var store = storeBuilder.build(); cancellationToken.checkCancelled(); -- cgit v1.2.3-54-g00ecf From 13b464db253566290be6a1063ad8e296288d3339 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy <kristof@marussy.com> Date: Thu, 14 Sep 2023 03:05:28 +0200 Subject: feat: specify random seed for generation --- subprojects/frontend/src/editor/AnimatedButton.tsx | 2 +- subprojects/frontend/src/editor/EditorStore.ts | 8 +++---- subprojects/frontend/src/editor/GenerateButton.tsx | 8 ++++++- .../frontend/src/editor/GeneratedModelStore.ts | 4 ++-- .../frontend/src/xtext/ModelGenerationService.ts | 26 +++++++++++++++++++--- subprojects/frontend/src/xtext/UpdateService.ts | 7 +++--- subprojects/frontend/src/xtext/XtextClient.ts | 5 +++-- .../frontend/src/xtext/XtextWebSocketClient.ts | 2 +- .../web/generator/ModelGenerationService.java | 4 ++-- .../web/generator/ModelGenerationWorker.java | 7 ++++-- .../server/ThreadPoolExecutorServiceProvider.java | 20 ++++++++++++++++- .../xtext/server/push/PushServiceDispatcher.java | 4 +++- .../store/dse/strategy/BestFirstStoreManager.java | 11 +++++++-- 13 files changed, 83 insertions(+), 25 deletions(-) (limited to 'subprojects/language-web/src/main/java/tools') diff --git a/subprojects/frontend/src/editor/AnimatedButton.tsx b/subprojects/frontend/src/editor/AnimatedButton.tsx index 24ec69be..606aabea 100644 --- a/subprojects/frontend/src/editor/AnimatedButton.tsx +++ b/subprojects/frontend/src/editor/AnimatedButton.tsx @@ -45,7 +45,7 @@ export default function AnimatedButton({ children, }: { 'aria-label'?: string; - onClick?: () => void; + onClick?: React.MouseEventHandler<HTMLElement>; color: 'error' | 'warning' | 'primary' | 'inherit'; disabled?: boolean; startIcon?: JSX.Element; diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index f9a9a7da..9508858d 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts @@ -313,14 +313,14 @@ export default class EditorStore { this.disposed = true; } - startModelGeneration(): void { + startModelGeneration(randomSeed?: number): void { this.client - ?.startModelGeneration() + ?.startModelGeneration(randomSeed) ?.catch((error) => log.error('Could not start model generation', error)); } - addGeneratedModel(uuid: string): void { - this.generatedModels.set(uuid, new GeneratedModelStore()); + addGeneratedModel(uuid: string, randomSeed: number): void { + this.generatedModels.set(uuid, new GeneratedModelStore(randomSeed)); this.selectGeneratedModel(uuid); } diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index b8dcd531..b6b1655a 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx @@ -98,7 +98,13 @@ const GenerateButton = observer(function GenerateButton({ disabled={!editorStore.opened} color={warningCount > 0 ? 'warning' : 'primary'} startIcon={<PlayArrowIcon />} - onClick={() => editorStore.startModelGeneration()} + onClick={(event) => { + if (event.shiftKey) { + editorStore.startModelGeneration(1); + } else { + editorStore.startModelGeneration(); + } + }} > {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} </AnimatedButton> diff --git a/subprojects/frontend/src/editor/GeneratedModelStore.ts b/subprojects/frontend/src/editor/GeneratedModelStore.ts index d0181eed..5088d603 100644 --- a/subprojects/frontend/src/editor/GeneratedModelStore.ts +++ b/subprojects/frontend/src/editor/GeneratedModelStore.ts @@ -18,9 +18,9 @@ export default class GeneratedModelStore { graph: GraphStore | undefined; - constructor() { + constructor(randomSeed: number) { const time = new Date().toLocaleTimeString(undefined, { hour12: false }); - this.title = `Generated at ${time}`; + this.title = `Generated at ${time} (${randomSeed})`; makeAutoObservable(this); } diff --git a/subprojects/frontend/src/xtext/ModelGenerationService.ts b/subprojects/frontend/src/xtext/ModelGenerationService.ts index 1e9f837a..29a70623 100644 --- a/subprojects/frontend/src/xtext/ModelGenerationService.ts +++ b/subprojects/frontend/src/xtext/ModelGenerationService.ts @@ -4,12 +4,18 @@ * SPDX-License-Identifier: EPL-2.0 */ +import type { Transaction } from '@codemirror/state'; + import type EditorStore from '../editor/EditorStore'; import type UpdateService from './UpdateService'; import { ModelGenerationResult } from './xtextServiceResults'; +const INITIAL_RANDOM_SEED = 1; + export default class ModelGenerationService { + private nextRandomSeed = INITIAL_RANDOM_SEED; + constructor( private readonly store: EditorStore, private readonly updateService: UpdateService, @@ -26,14 +32,24 @@ export default class ModelGenerationService { } } + onTransaction(transaction: Transaction): void { + if (transaction.docChanged) { + this.resetRandomSeed(); + } + } + onDisconnect(): void { this.store.modelGenerationCancelled(); + this.resetRandomSeed(); } - async start(): Promise<void> { - const result = await this.updateService.startModelGeneration(); + async start(randomSeed?: number): Promise<void> { + const randomSeedOrNext = randomSeed ?? this.nextRandomSeed; + this.nextRandomSeed = randomSeedOrNext + 1; + const result = + await this.updateService.startModelGeneration(randomSeedOrNext); if (!result.cancelled) { - this.store.addGeneratedModel(result.data.uuid); + this.store.addGeneratedModel(result.data.uuid, randomSeedOrNext); } } @@ -43,4 +59,8 @@ export default class ModelGenerationService { this.store.modelGenerationCancelled(); } } + + private resetRandomSeed() { + this.nextRandomSeed = INITIAL_RANDOM_SEED; + } } diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index d1246d5e..70e79764 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts @@ -343,9 +343,9 @@ export default class UpdateService { return { cancelled: false, data: parsedOccurrencesResult }; } - async startModelGeneration(): Promise< - CancellableResult<ModelGenerationStartedResult> - > { + async startModelGeneration( + randomSeed: number, + ): Promise<CancellableResult<ModelGenerationStartedResult>> { try { await this.updateOrThrow(); } catch (error) { @@ -360,6 +360,7 @@ export default class UpdateService { serviceType: 'modelGeneration', requiredStateId: this.xtextStateId, start: true, + randomSeed, }); if (isConflictResult(data)) { return { cancelled: true }; diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index 4df4f57a..7486d737 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts @@ -99,6 +99,7 @@ export default class XtextClient { this.contentAssistService.onTransaction(transaction); this.updateService.onTransaction(transaction); this.occurrencesService.onTransaction(transaction); + this.modelGenerationService.onTransaction(transaction); } private onPush( @@ -150,8 +151,8 @@ export default class XtextClient { return this.contentAssistService.contentAssist(context); } - startModelGeneration(): Promise<void> { - return this.modelGenerationService.start(); + startModelGeneration(randomSeed?: number): Promise<void> { + return this.modelGenerationService.start(randomSeed); } cancelModelGeneration(): Promise<void> { diff --git a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts index bb84223c..280ac875 100644 --- a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts +++ b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts @@ -204,7 +204,7 @@ export default class XtextWebSocketClient { get state() { this.stateAtom.reportObserved(); - return this.interpreter.state; + return this.interpreter.getSnapshot(); } get opening(): boolean { diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java index 5a60007f..9f72e462 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java @@ -34,13 +34,13 @@ public class ModelGenerationService { timeoutSec = SemanticsService.getTimeout("REFINERY_MODEL_GENERATION_TIMEOUT_SEC").orElse(600L); } - public ModelGenerationStartedResult generateModel(PushWebDocumentAccess document){ + public ModelGenerationStartedResult generateModel(PushWebDocumentAccess document, int randomSeed) { return document.modify(new CancelableUnitOfWork<>() { @Override public ModelGenerationStartedResult exec(IXtextWebDocument state, CancelIndicator cancelIndicator) { var pushState = (PushWebDocument) state; var worker = workerProvider.get(); - worker.setState(pushState, timeoutSec); + worker.setState(pushState, randomSeed, timeoutSec); var manager = pushState.getModelGenerationManager(); worker.start(); boolean canceled = manager.setActiveModelGenerationWorker(worker, cancelIndicator); diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java index 77fc7484..9ee74207 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java @@ -79,6 +79,8 @@ public class ModelGenerationWorker implements Runnable { private ScheduledExecutorService scheduledExecutorService; + private int randomSeed; + private long timeoutSec; private Future<?> future; @@ -97,8 +99,9 @@ public class ModelGenerationWorker implements Runnable { scheduledExecutorService = provider.getScheduled(ModelGenerationService.MODEL_GENERATION_TIMEOUT_EXECUTOR); } - public void setState(PushWebDocument state, long timeoutSec) { + public void setState(PushWebDocument state, int randomSeed, long timeoutSec) { this.state = state; + this.randomSeed = randomSeed; this.timeoutSec = timeoutSec; text = state.getText(); } @@ -188,7 +191,7 @@ public class ModelGenerationWorker implements Runnable { cancellationToken.checkCancelled(); notifyResult(new ModelGenerationStatusResult(uuid, "Generating model")); var bestFirst = new BestFirstStoreManager(store, 1); - bestFirst.startExploration(initialVersion); + bestFirst.startExploration(initialVersion, randomSeed); cancellationToken.checkCancelled(); var solutionStore = bestFirst.getSolutionStore(); if (solutionStore.getSolutions().isEmpty()) { diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java index 625909b9..ff8f4943 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java @@ -9,6 +9,9 @@ import com.google.inject.Singleton; import org.eclipse.xtext.ide.ExecutorServiceProvider; import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tools.refinery.language.web.generator.ModelGenerationService; import tools.refinery.language.web.semantics.SemanticsService; import java.lang.invoke.MethodHandle; @@ -25,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger; @Singleton public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { + private static final Logger LOG = LoggerFactory.getLogger(ThreadPoolExecutorServiceProvider.class); private static final String DOCUMENT_LOCK_EXECUTOR; private static final AtomicInteger POOL_ID = new AtomicInteger(1); @@ -33,6 +37,7 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { private final int executorThreadCount; private final int lockExecutorThreadCount; private final int semanticsExecutorThreadCount; + private final int generatorExecutorThreadCount; static { var lookup = MethodHandles.lookup(); @@ -57,7 +62,18 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { public ThreadPoolExecutorServiceProvider() { executorThreadCount = getCount("REFINERY_XTEXT_THREAD_COUNT").orElse(0); lockExecutorThreadCount = getCount("REFINERY_XTEXT_LOCKING_THREAD_COUNT").orElse(executorThreadCount); - semanticsExecutorThreadCount = getCount("REFINERY_XTEXT_SEMANTICS_THREAD_COUNT").orElse(executorThreadCount); + int semanticsCount = getCount("REFINERY_XTEXT_SEMANTICS_THREAD_COUNT").orElse(0); + if (semanticsCount == 0 || executorThreadCount == 0) { + semanticsExecutorThreadCount = 0; + } else { + semanticsExecutorThreadCount = Math.max(semanticsCount, executorThreadCount); + } + if (semanticsExecutorThreadCount != semanticsCount) { + LOG.warn("Setting REFINERY_XTEXT_SEMANTICS_THREAD_COUNT to {} to avoid deadlock. This value must be " + + "either 0 or at least as large as REFINERY_XTEXT_THREAD_COUNT to avoid lock contention.", + semanticsExecutorThreadCount); + } + generatorExecutorThreadCount = getCount("REFINERY_MODEL_GENERATION_THREAD_COUNT").orElse(executorThreadCount); } private static Optional<Integer> getCount(String name) { @@ -94,6 +110,8 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { private int getSize(String key) { if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) { return semanticsExecutorThreadCount; + } else if (ModelGenerationService.MODEL_GENERATION_EXECUTOR.equals(key)) { + return generatorExecutorThreadCount; } else if (DOCUMENT_LOCK_EXECUTOR.equals(key)) { return lockExecutorThreadCount; } else { 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 a04ee226..e1d00d8f 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 @@ -62,11 +62,13 @@ public class PushServiceDispatcher extends XtextServiceDispatcher { if (!start && !cancel) { throw new InvalidRequestException("Either start of cancel must be specified"); } + @SuppressWarnings({"squid:S4738"}) + int randomSeed = start ? getInt(context, "randomSeed", Optional.absent()) : 0; var descriptor = new ServiceDescriptor(); descriptor.setService(() -> { try { if (start) { - return modelGenerationService.generateModel(document); + return modelGenerationService.generateModel(document, randomSeed); } else { return modelGenerationService.cancelModelGeneration(document); } diff --git a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStoreManager.java b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStoreManager.java index 02634a02..3d32f84c 100644 --- a/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStoreManager.java +++ b/subprojects/store-dse/src/main/java/tools/refinery/store/dse/strategy/BestFirstStoreManager.java @@ -44,11 +44,13 @@ public class BestFirstStoreManager { equivalenceClassStore = new FastEquivalenceClassStore(modelStore.getAdapter(StateCoderStoreAdapter.class)) { @Override protected void delegate(VersionWithObjectiveValue version, int[] emptyActivations, boolean accept) { - throw new UnsupportedOperationException("This equivalence storage is not prepared to resolve symmetries!"); + throw new UnsupportedOperationException("This equivalence storage is not prepared to resolve " + + "symmetries!"); } }; visualizationStore = new VisualizationStoreImpl(); } + public ModelStore getModelStore() { return modelStore; } @@ -74,7 +76,12 @@ public class BestFirstStoreManager { } public void startExploration(Version initial) { - BestFirstExplorer bestFirstExplorer = new BestFirstExplorer(this, modelStore.createModelForState(initial), 1); + startExploration(initial, 1); + } + + public void startExploration(Version initial, int randomSeed) { + BestFirstExplorer bestFirstExplorer = new BestFirstExplorer(this, modelStore.createModelForState(initial), + randomSeed); bestFirstExplorer.explore(); } } -- cgit v1.2.3-54-g00ecf