/* * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ package tools.refinery.language.semantics; import com.google.inject.Inject; import com.google.inject.Provider; import org.eclipse.collections.api.factory.primitive.IntObjectMaps; import org.eclipse.collections.api.map.primitive.MutableIntObjectMap; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.xtext.naming.IQualifiedNameProvider; import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.resource.FileExtensionProvider; import org.eclipse.xtext.resource.IResourceFactory; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.resource.XtextResourceSet; import org.eclipse.xtext.scoping.IScopeProvider; import tools.refinery.language.model.problem.*; import tools.refinery.language.utils.ProblemDesugarer; import tools.refinery.language.utils.ProblemUtil; import tools.refinery.store.model.Model; import tools.refinery.store.reasoning.ReasoningAdapter; import tools.refinery.store.reasoning.interpretation.PartialInterpretation; import tools.refinery.store.reasoning.literal.Concreteness; import tools.refinery.store.reasoning.representation.PartialRelation; import tools.refinery.store.reasoning.translator.typehierarchy.InferredType; import tools.refinery.store.reasoning.translator.typehierarchy.TypeHierarchyTranslator; import tools.refinery.store.representation.TruthValue; import tools.refinery.store.tuple.Tuple; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; import java.util.function.Function; import java.util.stream.Collectors; public class SolutionSerializer { private String fileExtension; @Inject private Provider resourceSetProvider; @Inject private IResourceFactory resourceFactory; @Inject private IQualifiedNameProvider qualifiedNameProvider; @Inject private SemanticsUtils semanticsUtils; @Inject private IScopeProvider scopeProvider; @Inject private ProblemDesugarer desugarer; @Inject private NodeNameProvider nameProvider; private ProblemTrace trace; private Model model; private ReasoningAdapter reasoningAdapter; private PartialInterpretation existsInterpretation; private Problem problem; private final MutableIntObjectMap nodes = IntObjectMaps.mutable.empty(); @Inject public void setFileExtensionProvider(FileExtensionProvider fileExtensionProvider) { this.fileExtension = fileExtensionProvider.getPrimaryFileExtension(); } public Problem serializeSolution(ProblemTrace trace, Model model) { var uri = URI.createFileURI("__synthetic" + fileExtension); return serializeSolution(trace, model, uri); } public Problem serializeSolution(ProblemTrace trace, Model model, URI uri) { this.trace = trace; this.model = model; reasoningAdapter = model.getAdapter(ReasoningAdapter.class); existsInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, ReasoningAdapter.EXISTS_SYMBOL); var originalProblem = trace.getProblem(); problem = copyProblem(originalProblem, uri); problem.getStatements().removeIf(SolutionSerializer::shouldRemoveStatement); problem.getNodes().removeIf(this::shouldRemoveNode); nameProvider.setProblem(problem); addExistsAssertions(); addClassAssertions(); addReferenceAssertions(); return problem; } private static boolean shouldRemoveStatement(Statement statement) { return statement instanceof Assertion || statement instanceof ScopeDeclaration; } private boolean shouldRemoveNode(Node newNode) { var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(newNode); var scope = scopeProvider.getScope(trace.getProblem(), ProblemPackage.Literals.ASSERTION__RELATION); var originalNode = semanticsUtils.maybeGetElement(problem, scope, qualifiedName, Node.class); if (originalNode == null) { return false; } int nodeId = trace.getNodeId(originalNode); return !isExistingNode(nodeId); } private boolean isExistingNode(int nodeId) { var exists = existsInterpretation.get(Tuple.of(nodeId)); if (!exists.isConcrete()) { throw new IllegalStateException("Invalid EXISTS %s for node %d".formatted(exists, nodeId)); } return exists.may(); } private Problem copyProblem(Problem originalProblem, URI uri) { var newResourceSet = resourceSetProvider.get(); if (!fileExtension.equals(uri.fileExtension())) { uri = uri.appendFileExtension(fileExtension); } var newResource = resourceFactory.createResource(uri); newResourceSet.getResources().add(newResource); var originalResource = originalProblem.eResource(); if (originalResource instanceof XtextResource) { byte[] bytes; try { try (var outputStream = new ByteArrayOutputStream()) { originalResource.save(outputStream, Map.of()); bytes = outputStream.toByteArray(); } try (var inputStream = new ByteArrayInputStream(bytes)) { newResource.load(inputStream, Map.of()); } } catch (IOException e) { throw new IllegalStateException("Failed to copy problem", e); } var contents = newResource.getContents(); if (!contents.isEmpty() && contents.getFirst() instanceof Problem newProblem) { return newProblem; } throw new IllegalStateException("Invalid contents of copied problem"); } else { var newProblem = EcoreUtil.copy(originalProblem); newResource.getContents().add(newProblem); return newProblem; } } private Relation findRelation(Relation originalRelation) { var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(originalRelation); var scope = scopeProvider.getScope(problem, ProblemPackage.Literals.ASSERTION__RELATION); return semanticsUtils.getElement(problem, scope, qualifiedName, Relation.class); } private Relation findPartialRelation(PartialRelation partialRelation) { return findRelation(trace.getRelation(partialRelation)); } private Node findNode(Node originalNode) { var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(originalNode); return findNode(qualifiedName); } private Node findNode(QualifiedName qualifiedName) { var scope = scopeProvider.getScope(problem, ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE); return semanticsUtils.maybeGetElement(problem, scope, qualifiedName, Node.class); } private void addAssertion(Relation relation, LogicValue value, Node... arguments) { var assertion = ProblemFactory.eINSTANCE.createAssertion(); assertion.setRelation(relation); for (var node : arguments) { var argument = ProblemFactory.eINSTANCE.createNodeAssertionArgument(); argument.setNode(node); assertion.getArguments().add(argument); } var logicConstant = ProblemFactory.eINSTANCE.createLogicConstant(); logicConstant.setLogicValue(value); assertion.setValue(logicConstant); problem.getStatements().add(assertion); } private void addExistsAssertions() { var builtinSymbols = desugarer.getBuiltinSymbols(problem) .orElseThrow(() -> new IllegalStateException("No builtin library in copied problem")); // Make sure to output exists assertions in a deterministic order. var sortedNewNodes = new TreeMap(); for (var pair : trace.getNodeTrace().keyValuesView()) { var originalNode = pair.getOne(); int nodeId = pair.getTwo(); var newNode = findNode(originalNode); // Since all implicit nodes that do not exist has already been remove in serializeSolution, // we only need to add !exists assertions to ::new nodes (nodes marked as an individual must always exist). if (ProblemUtil.isNewNode(originalNode)) { sortedNewNodes.put(nodeId, newNode); } else { nodes.put(nodeId, newNode); } } for (var newNode : sortedNewNodes.values()) { // If a node is a new node of the class, we should replace it with a normal node. addAssertion(builtinSymbols.exists(), LogicValue.FALSE, newNode); } } private void addClassAssertions() { var types = trace.getMetamodel().typeHierarchy().getPreservedTypes().keySet().stream() .collect(Collectors.toMap(Function.identity(), this::findPartialRelation)); var cursor = model.getInterpretation(TypeHierarchyTranslator.TYPE_SYMBOL).getAll(); while (cursor.move()) { var key = cursor.getKey(); var nodeId = key.get(0); if (isExistingNode(nodeId)) { createNodeAndAssertType(nodeId, cursor.getValue(), types); } } } private void createNodeAndAssertType(int nodeId, InferredType inferredType, Map types) { var candidateTypeSymbol = inferredType.candidateType(); var candidateRelation = types.get(candidateTypeSymbol); if (candidateRelation instanceof EnumDeclaration) { // Type assertions for enum literals are added implicitly. return; } Node node = nodes.get(nodeId); if (node == null) { var typeName = candidateRelation.getName(); var nodeName = nameProvider.getNextName(typeName); node = ProblemFactory.eINSTANCE.createNode(); node.setName(nodeName); problem.getNodes().add(node); nodes.put(nodeId, node); } addAssertion(candidateRelation, LogicValue.TRUE, node); var typeAnalysisResult = trace.getMetamodel().typeHierarchy().getPreservedTypes().get(candidateTypeSymbol); for (var subtype : typeAnalysisResult.getDirectSubtypes()) { var subtypeRelation = types.get(subtype); addAssertion(subtypeRelation, LogicValue.FALSE, node); } } private void addReferenceAssertions() { var metamodel = trace.getMetamodel(); for (var partialRelation : metamodel.containmentHierarchy().keySet()) { // No need to add a default value, because in a concrete model, each contained node has only a single // container. addAssertions(partialRelation); } for (var partialRelation : metamodel.directedCrossReferences().keySet()) { addDefaultAssertion(partialRelation); addAssertions(partialRelation); } // No need to add directed opposite references, because their default value is {@code unknown} and their // actual value will always be computed from the value of the directed forward reference. // However, undirected cross-references have to be serialized in both directions due to the default value of // {@code false}. for (var partialRelation : metamodel.undirectedCrossReferences().keySet()) { addDefaultAssertion(partialRelation); addAssertions(partialRelation); } } private void addAssertions(PartialRelation partialRelation) { var relation = findPartialRelation(partialRelation); var cursor = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, partialRelation).getAll(); // Make sure to output assertions in a deterministic order. var sortedTuples = new TreeSet(); while (cursor.move()) { var tuple = cursor.getKey(); var from = nodes.get(tuple.get(0)); var to = nodes.get(tuple.get(1)); if (from == null || to == null) { // One of the endpoints does not exist in the candidate model. continue; } var value = cursor.getValue(); if (!value.isConcrete()) { throw new IllegalStateException("Invalid %s %s for tuple %s".formatted(partialRelation, value, tuple)); } if (value.may()) { sortedTuples.add(tuple); } } for (var tuple : sortedTuples) { var from = nodes.get(tuple.get(0)); var to = nodes.get(tuple.get(1)); addAssertion(relation, LogicValue.TRUE, from, to); } } private void addDefaultAssertion(PartialRelation partialRelation) { var relation = findPartialRelation(partialRelation); var assertion = ProblemFactory.eINSTANCE.createAssertion(); assertion.setDefault(true); assertion.setRelation(relation); int arity = ProblemUtil.getArity(relation); for (int i = 0; i < arity; i++) { var argument = ProblemFactory.eINSTANCE.createWildcardAssertionArgument(); assertion.getArguments().add(argument); } var logicConstant = ProblemFactory.eINSTANCE.createLogicConstant(); logicConstant.setLogicValue(LogicValue.FALSE); assertion.setValue(logicConstant); problem.getStatements().add(assertion); } }