From d5654f1ae03bec95c08e69a19a116c9825a27098 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sat, 3 Feb 2024 01:39:11 +0100 Subject: feat(language): import resolution --- .../refinery/language/ProblemRuntimeModule.java | 1 + .../refinery/language/library/BuiltinLibrary.java | 41 ++++ .../language/library/RefineryLibraries.java | 54 +++++ .../refinery/language/library/RefineryLibrary.java | 20 ++ .../language/resource/DerivedVariableComputer.java | 130 ------------ .../language/resource/ImplicitVariableScope.java | 143 ------------- .../LoadOnDemandResourceDescriptionProvider.java | 49 +++++ .../language/resource/NodeNameCollector.java | 79 -------- .../resource/ProblemDerivedStateComputer.java | 222 --------------------- .../ProblemResourceDescriptionStrategy.java | 12 ++ .../resource/state/DerivedVariableComputer.java | 130 ++++++++++++ .../resource/state/ImplicitVariableScope.java | 143 +++++++++++++ .../language/resource/state/NodeNameCollector.java | 79 ++++++++ .../state/ProblemDerivedStateComputer.java | 222 +++++++++++++++++++++ .../language/scoping/CompositeSelectable.java | 62 ++++++ .../language/scoping/NormalizedSelectable.java | 37 ++-- .../scoping/ProblemGlobalScopeProvider.java | 84 ++++++-- .../scoping/ProblemLocalScopeProvider.java | 65 ++++-- .../refinery/language/scoping/imports/Import.java | 12 ++ .../language/scoping/imports/ImportCollection.java | 76 +++++++ .../language/scoping/imports/ImportCollector.java | 138 +++++++++++++ .../language/scoping/imports/NamedImport.java | 22 ++ .../language/scoping/imports/TransitiveImport.java | 11 + .../refinery/language/utils/ProblemDesugarer.java | 7 +- .../tools/refinery/language/utils/ProblemUtil.java | 16 +- ...tools.refinery.language.library.RefineryLibrary | 4 + .../tools/refinery/language/builtin.problem | 16 -- .../refinery/language/library/builtin.refinery | 16 ++ 28 files changed, 1227 insertions(+), 664 deletions(-) create mode 100644 subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibraries.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/DerivedVariableComputer.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/ImplicitVariableScope.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/NodeNameCollector.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/state/DerivedVariableComputer.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/state/ImplicitVariableScope.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/state/NodeNameCollector.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/resource/state/ProblemDerivedStateComputer.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/scoping/CompositeSelectable.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/scoping/imports/Import.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollection.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/scoping/imports/NamedImport.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/scoping/imports/TransitiveImport.java create mode 100644 subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary delete mode 100644 subprojects/language/src/main/resources/tools/refinery/language/builtin.problem create mode 100644 subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery (limited to 'subprojects/language') diff --git a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java index 19816da4..a846e265 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java +++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java @@ -32,6 +32,7 @@ import tools.refinery.language.naming.ProblemQualifiedNameConverter; import tools.refinery.language.naming.ProblemQualifiedNameProvider; import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; import tools.refinery.language.resource.*; +import tools.refinery.language.resource.state.ProblemDerivedStateComputer; import tools.refinery.language.scoping.ProblemGlobalScopeProvider; import tools.refinery.language.scoping.ProblemLocalScopeProvider; import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java b/subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java new file mode 100644 index 00000000..0fb4cd2c --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.library; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.naming.QualifiedName; + +import java.util.List; +import java.util.Optional; + +public class BuiltinLibrary implements RefineryLibrary { + public static final QualifiedName BUILTIN_LIBRARY_NAME = QualifiedName.create("builtin"); + public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(BUILTIN_LIBRARY_NAME).orElseThrow( + () -> new IllegalStateException("Builtin library was not found")); + + @Override + public List getAutomaticImports() { + return List.of(BUILTIN_LIBRARY_NAME); + } + + @Override + public Optional resolveQualifiedName(QualifiedName qualifiedName) { + if (qualifiedName.startsWith(BUILTIN_LIBRARY_NAME)) { + return getLibraryUri(qualifiedName); + } + return Optional.empty(); + } + + private static Optional getLibraryUri(QualifiedName qualifiedName) { + var libraryPath = String.join("/", qualifiedName.getSegments()); + var libraryResource = BuiltinLibrary.class.getClassLoader() + .getResource("tools/refinery/language/library/%s.refinery".formatted(libraryPath)); + if (libraryResource == null) { + return Optional.empty(); + } + return Optional.of(URI.createURI(libraryResource.toString())); + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibraries.java b/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibraries.java new file mode 100644 index 00000000..0efca199 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibraries.java @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.library; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.naming.QualifiedName; +import tools.refinery.language.scoping.imports.NamedImport; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; + +public final class RefineryLibraries { + private static final ServiceLoader SERVICE_LOADER = ServiceLoader.load(RefineryLibrary.class); + private static final List AUTOMATIC_IMPORTS; + + static { + var imports = new LinkedHashMap(); + for (var service : SERVICE_LOADER) { + for (var qualifiedName : service.getAutomaticImports()) { + var uri = service.resolveQualifiedName(qualifiedName).orElseThrow( + () -> new IllegalStateException("Automatic import %s was not found".formatted(qualifiedName))); + if (imports.put(qualifiedName, uri) != null) { + throw new IllegalStateException("Duplicate automatic import " + qualifiedName); + } + } + } + AUTOMATIC_IMPORTS = imports.entrySet().stream() + .map(entry -> NamedImport.implicit(entry.getValue(), entry.getKey())) + .toList(); + } + + private RefineryLibraries() { + throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); + } + + public static List getAutomaticImports() { + return AUTOMATIC_IMPORTS; + } + + public static Optional resolveQualifiedName(QualifiedName qualifiedName) { + for (var service : SERVICE_LOADER) { + var result = service.resolveQualifiedName(qualifiedName); + if (result.isPresent()) { + return result; + } + } + return Optional.empty(); + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java b/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java new file mode 100644 index 00000000..9db2900e --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.library; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.naming.QualifiedName; + +import java.util.List; +import java.util.Optional; + +public interface RefineryLibrary { + default List getAutomaticImports() { + return List.of(); + } + + Optional resolveQualifiedName(QualifiedName qualifiedName); +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/DerivedVariableComputer.java b/subprojects/language/src/main/java/tools/refinery/language/resource/DerivedVariableComputer.java deleted file mode 100644 index 07c5da41..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/DerivedVariableComputer.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.resource; - -import com.google.inject.Inject; -import com.google.inject.Singleton; -import com.google.inject.name.Named; -import org.eclipse.xtext.linking.impl.LinkingHelper; -import org.eclipse.xtext.naming.IQualifiedNameConverter; -import org.eclipse.xtext.scoping.IScopeProvider; -import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; -import tools.refinery.language.model.problem.*; - -import java.util.*; - -@Singleton -public class DerivedVariableComputer { - @Inject - private LinkingHelper linkingHelper; - - @Inject - private IQualifiedNameConverter qualifiedNameConverter; - - @Inject - @Named(AbstractDeclarativeScopeProvider.NAMED_DELEGATE) - private IScopeProvider scopeProvider; - - public void installDerivedVariables(Problem problem, Set nodeNames) { - for (Statement statement : problem.getStatements()) { - if (statement instanceof ParametricDefinition definition) { - installDerivedParametricDefinitionState(definition, nodeNames); - } - } - } - - protected void installDerivedParametricDefinitionState(ParametricDefinition definition, Set nodeNames) { - Set knownVariables = new HashSet<>(nodeNames); - for (Parameter parameter : definition.getParameters()) { - String name = parameter.getName(); - if (name != null) { - knownVariables.add(name); - } - } - if (definition instanceof PredicateDefinition predicateDefinition) { - installDerivedPredicateDefinitionState(predicateDefinition, knownVariables); - } else if (definition instanceof FunctionDefinition functionDefinition) { - installDerivedFunctionDefinitionState(functionDefinition, knownVariables); - } else if (definition instanceof RuleDefinition ruleDefinition) { - installDerivedRuleDefinitionState(ruleDefinition, knownVariables); - } else { - throw new IllegalArgumentException("Unknown ParametricDefinition: " + definition); - } - } - - protected void installDerivedPredicateDefinitionState(PredicateDefinition definition, Set knownVariables) { - for (Conjunction body : definition.getBodies()) { - createVariablesForScope(new ImplicitVariableScope(body, knownVariables)); - } - } - - protected void installDerivedFunctionDefinitionState(FunctionDefinition definition, Set knownVariables) { - for (Case body : definition.getCases()) { - if (body instanceof Conjunction conjunction) { - createVariablesForScope(new ImplicitVariableScope(conjunction, knownVariables)); - } else if (body instanceof Match match) { - var condition = match.getCondition(); - if (condition != null) { - createVariablesForScope(new ImplicitVariableScope(match, match.getCondition(), knownVariables)); - } - } else { - throw new IllegalArgumentException("Unknown Case: " + body); - } - } - } - - protected void installDerivedRuleDefinitionState(RuleDefinition definition, Set knownVariables) { - for (Conjunction precondition : definition.getPreconditions()) { - createVariablesForScope(new ImplicitVariableScope(precondition, knownVariables)); - } - } - - protected void createVariablesForScope(ImplicitVariableScope scope) { - var queue = new ArrayDeque(); - queue.addLast(scope); - while (!queue.isEmpty()) { - var nextScope = queue.removeFirst(); - nextScope.createVariables(scopeProvider, linkingHelper, qualifiedNameConverter, queue); - } - } - - public void discardDerivedVariables(Problem problem) { - for (Statement statement : problem.getStatements()) { - if (statement instanceof ParametricDefinition parametricDefinition) { - discardParametricDefinitionState(parametricDefinition); - } - } - } - - protected void discardParametricDefinitionState(ParametricDefinition definition) { - List existentialQuantifiers = new ArrayList<>(); - List variableOrNodeExprs = new ArrayList<>(); - var treeIterator = definition.eAllContents(); - // We must collect the nodes where we are discarding derived state and only discard them after the iteration, - // because modifying the containment hierarchy during iteration causes the TreeIterator to fail with - // IndexOutOfBoundsException. - while (treeIterator.hasNext()) { - var child = treeIterator.next(); - var containingFeature = child.eContainingFeature(); - if (containingFeature == ProblemPackage.Literals.EXISTENTIAL_QUANTIFIER__IMPLICIT_VARIABLES || - containingFeature == ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__SINGLETON_VARIABLE) { - treeIterator.prune(); - } else if (child instanceof ExistentialQuantifier existentialQuantifier && - !existentialQuantifier.getImplicitVariables().isEmpty()) { - existentialQuantifiers.add(existentialQuantifier); - } else if (child instanceof VariableOrNodeExpr variableOrNodeExpr && - variableOrNodeExpr.getSingletonVariable() != null) { - variableOrNodeExprs.add(variableOrNodeExpr); - } - } - for (var existentialQuantifier : existentialQuantifiers) { - existentialQuantifier.getImplicitVariables().clear(); - } - for (var variableOrNodeExpr : variableOrNodeExprs) { - variableOrNodeExpr.setSingletonVariable(null); - } - } -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ImplicitVariableScope.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ImplicitVariableScope.java deleted file mode 100644 index e97c8287..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ImplicitVariableScope.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.resource; - -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.linking.impl.LinkingHelper; -import org.eclipse.xtext.naming.IQualifiedNameConverter; -import org.eclipse.xtext.naming.QualifiedName; -import org.eclipse.xtext.nodemodel.INode; -import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.eclipse.xtext.scoping.IScope; -import org.eclipse.xtext.scoping.IScopeProvider; -import tools.refinery.language.model.problem.*; -import tools.refinery.language.naming.NamingUtil; - -import java.util.Deque; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class ImplicitVariableScope { - private final EObject root; - - private final ExistentialQuantifier quantifier; - - private final ImplicitVariableScope parent; - - private Set knownVariables; - - private ImplicitVariableScope(ExistentialQuantifier quantifier, ImplicitVariableScope parent) { - this.root = quantifier; - this.quantifier = quantifier; - this.parent = parent; - this.knownVariables = null; - } - - public ImplicitVariableScope(EObject root, ExistentialQuantifier quantifier, Set knownVariables) { - this.root = root; - this.quantifier = quantifier; - this.parent = null; - this.knownVariables = new HashSet<>(knownVariables); - } - - public ImplicitVariableScope(ExistentialQuantifier root, Set knownVariables) { - this(root, root, knownVariables); - } - - public void createVariables(IScopeProvider scopeProvider, LinkingHelper linkingHelper, - IQualifiedNameConverter qualifiedNameConverter, - Deque scopeQueue) { - initializeKnownVariables(); - processEObject(root, scopeProvider, linkingHelper, qualifiedNameConverter); - var treeIterator = root.eAllContents(); - while (treeIterator.hasNext()) { - var child = treeIterator.next(); - if (child instanceof ExistentialQuantifier nestedQuantifier) { - scopeQueue.addLast(new ImplicitVariableScope(nestedQuantifier, this)); - treeIterator.prune(); - } else { - processEObject(child, scopeProvider, linkingHelper, qualifiedNameConverter); - } - } - } - - private void initializeKnownVariables() { - boolean hasKnownVariables = knownVariables != null; - boolean hasParent = parent != null; - if ((hasKnownVariables && hasParent) || (!hasKnownVariables && !hasParent)) { - throw new IllegalStateException("Either known variables or parent must be provided, but not both"); - } - if (hasKnownVariables) { - return; - } - if (parent.knownVariables == null) { - throw new IllegalStateException("Parent scope must be processed before current scope"); - } - knownVariables = new HashSet<>(parent.knownVariables); - } - - private void processEObject(EObject eObject, IScopeProvider scopeProvider, LinkingHelper linkingHelper, - IQualifiedNameConverter qualifiedNameConverter) { - if (!(eObject instanceof VariableOrNodeExpr variableOrNodeExpr)) { - return; - } - IScope scope = scopeProvider.getScope(variableOrNodeExpr, - ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE); - List nodes = NodeModelUtils.findNodesForFeature(variableOrNodeExpr, - ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE); - for (INode node : nodes) { - var variableName = linkingHelper.getCrossRefNodeAsString(node, true); - var created = tryCreateVariableForArgument(variableOrNodeExpr, variableName, qualifiedNameConverter, - scope); - if (created) { - break; - } - } - } - - protected boolean tryCreateVariableForArgument(VariableOrNodeExpr variableOrNodeExpr, String variableName, - IQualifiedNameConverter qualifiedNameConverter, IScope scope) { - if (!NamingUtil.isValidId(variableName)) { - return false; - } - QualifiedName qualifiedName; - try { - qualifiedName = qualifiedNameConverter.toQualifiedName(variableName); - } catch (IllegalArgumentException e) { - return false; - } - if (scope.getSingleElement(qualifiedName) != null) { - return false; - } - if (NamingUtil.isSingletonVariableName(variableName)) { - createSingletonVariable(variableOrNodeExpr, variableName); - return true; - } - if (!knownVariables.contains(variableName)) { - createVariable(variableName); - return true; - } - return false; - } - - protected void createVariable(String variableName) { - knownVariables.add(variableName); - ImplicitVariable variable = createNamedVariable(variableName); - quantifier.getImplicitVariables().add(variable); - } - - protected void createSingletonVariable(VariableOrNodeExpr variableOrNodeExpr, String variableName) { - ImplicitVariable variable = createNamedVariable(variableName); - variableOrNodeExpr.setSingletonVariable(variable); - } - - protected ImplicitVariable createNamedVariable(String variableName) { - var variable = ProblemFactory.eINSTANCE.createImplicitVariable(); - variable.setName(variableName); - return variable; - } -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java b/subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java new file mode 100644 index 00000000..373a32f2 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.resource; + +import com.google.inject.Inject; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.resource.IResourceDescription; +import org.eclipse.xtext.resource.IResourceDescriptions; +import org.eclipse.xtext.resource.IResourceDescriptionsProvider; +import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider; + +public class LoadOnDemandResourceDescriptionProvider { + @Inject + private IResourceDescriptionsProvider resourceDescriptionsProvider; + + @Inject + private GlobalResourceDescriptionProvider globalResourceDescriptionProvider; + + private Resource context; + private IResourceDescriptions resourceDescriptions; + + public void setContext(Resource context) { + if (this.context != null) { + throw new IllegalStateException("Context was already set"); + } + this.context = context; + resourceDescriptions = resourceDescriptionsProvider.getResourceDescriptions(context.getResourceSet()); + } + + public IResourceDescription getResourceDescription(URI uri) { + if (this.context == null) { + throw new IllegalStateException("Context was not set"); + } + var resourceDescription = resourceDescriptions.getResourceDescription(uri); + if (resourceDescription != null) { + return resourceDescription; + } + var importedResource = EcoreUtil2.getResource(context, uri.toString()); + if (importedResource == null) { + return null; + } + return globalResourceDescriptionProvider.getResourceDescription(importedResource); + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/NodeNameCollector.java b/subprojects/language/src/main/java/tools/refinery/language/resource/NodeNameCollector.java deleted file mode 100644 index e5deca4d..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/NodeNameCollector.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.resource; - -import com.google.common.collect.ImmutableSet; -import com.google.inject.Inject; -import com.google.inject.name.Named; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.linking.impl.LinkingHelper; -import org.eclipse.xtext.naming.IQualifiedNameConverter; -import org.eclipse.xtext.nodemodel.INode; -import org.eclipse.xtext.nodemodel.util.NodeModelUtils; -import org.eclipse.xtext.scoping.IScope; -import org.eclipse.xtext.scoping.IScopeProvider; -import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; -import tools.refinery.language.model.problem.*; -import tools.refinery.language.naming.NamingUtil; - -import java.util.List; -import java.util.Set; - -public class NodeNameCollector { - @Inject - private LinkingHelper linkingHelper; - - @Inject - private IQualifiedNameConverter qualifiedNameConverter; - - @Inject - @Named(AbstractDeclarativeScopeProvider.NAMED_DELEGATE) - private IScopeProvider scopeProvider; - - private final ImmutableSet.Builder nodeNames = ImmutableSet.builder(); - - private IScope nodeScope; - - public Set getNodeNames() { - return nodeNames.build(); - } - - public void collectNodeNames(Problem problem) { - nodeScope = scopeProvider.getScope(problem, ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE); - for (Statement statement : problem.getStatements()) { - collectStatementNodeNames(statement); - } - } - - protected void collectStatementNodeNames(Statement statement) { - if (statement instanceof Assertion assertion) { - collectAssertionNodeNames(assertion); - } - } - - protected void collectAssertionNodeNames(Assertion assertion) { - for (AssertionArgument argument : assertion.getArguments()) { - if (argument instanceof NodeAssertionArgument) { - collectNodeNames(argument); - } - } - } - - private void collectNodeNames(EObject eObject) { - List nodes = NodeModelUtils.findNodesForFeature(eObject, - ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE); - for (INode node : nodes) { - var nodeName = linkingHelper.getCrossRefNodeAsString(node, true); - if (!NamingUtil.isValidId(nodeName)) { - continue; - } - var qualifiedName = qualifiedNameConverter.toQualifiedName(nodeName); - if (nodeScope.getSingleElement(qualifiedName) == null) { - nodeNames.add(nodeName); - } - } - } -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java deleted file mode 100644 index 31eb55a6..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.resource; - -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import com.google.inject.name.Named; -import org.eclipse.emf.common.notify.impl.AdapterImpl; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.emf.ecore.util.EcoreUtil; -import org.eclipse.xtext.Constants; -import org.eclipse.xtext.resource.DerivedStateAwareResource; -import org.eclipse.xtext.resource.IDerivedStateComputer; -import org.eclipse.xtext.resource.XtextResource; -import tools.refinery.language.model.problem.*; -import tools.refinery.language.utils.ProblemUtil; - -import java.util.*; -import java.util.function.Function; - -@Singleton -public class ProblemDerivedStateComputer implements IDerivedStateComputer { - public static final String NEW_NODE = "new"; - - @Inject - @Named(Constants.LANGUAGE_NAME) - private String languageName; - - @Inject - private Provider nodeNameCollectorProvider; - - @Inject - private DerivedVariableComputer derivedVariableComputer; - - @Override - public void installDerivedState(DerivedStateAwareResource resource, boolean preLinkingPhase) { - var problem = getProblem(resource); - if (problem != null) { - var adapter = getOrInstallAdapter(resource); - installDerivedProblemState(problem, adapter, preLinkingPhase); - } - } - - protected Problem getProblem(Resource resource) { - List contents = resource.getContents(); - if (contents.isEmpty()) { - return null; - } - EObject object = contents.get(0); - if (object instanceof Problem problem) { - return problem; - } - return null; - } - - protected void installDerivedProblemState(Problem problem, Adapter adapter, boolean preLinkingPhase) { - installDerivedClassDeclarationState(problem, adapter); - if (preLinkingPhase) { - return; - } - Set nodeNames = installDerivedNodes(problem); - derivedVariableComputer.installDerivedVariables(problem, nodeNames); - } - - protected void installDerivedClassDeclarationState(Problem problem, Adapter adapter) { - for (var statement : problem.getStatements()) { - if (statement instanceof ClassDeclaration classDeclaration) { - installOrRemoveNewNode(adapter, classDeclaration); - for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { - if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) { - installOrRemoveInvalidMultiplicityPredicate(adapter, classDeclaration, referenceDeclaration); - } - } - } - } - } - - protected void installOrRemoveNewNode(Adapter adapter, ClassDeclaration declaration) { - if (declaration.isAbstract()) { - var newNode = declaration.getNewNode(); - if (newNode != null) { - declaration.setNewNode(null); - adapter.removeNewNode(declaration); - } - } else { - if (declaration.getNewNode() == null) { - var newNode = adapter.createNewNodeIfAbsent(declaration, key -> createNode(NEW_NODE)); - declaration.setNewNode(newNode); - } - } - } - - protected void installOrRemoveInvalidMultiplicityPredicate( - Adapter adapter, ClassDeclaration containingClassDeclaration, ReferenceDeclaration declaration) { - if (ProblemUtil.hasMultiplicityConstraint(declaration)) { - if (declaration.getInvalidMultiplicity() == null) { - var invalidMultiplicity = adapter.createInvalidMultiplicityPredicateIfAbsent(declaration, key -> { - var predicate = ProblemFactory.eINSTANCE.createPredicateDefinition(); - predicate.setError(true); - predicate.setName("invalidMultiplicity"); - var parameter = ProblemFactory.eINSTANCE.createParameter(); - parameter.setParameterType(containingClassDeclaration); - parameter.setName("node"); - predicate.getParameters().add(parameter); - return predicate; - }); - declaration.setInvalidMultiplicity(invalidMultiplicity); - } - } else { - var invalidMultiplicity = declaration.getInvalidMultiplicity(); - if (invalidMultiplicity != null) { - declaration.setInvalidMultiplicity(null); - adapter.removeInvalidMultiplicityPredicate(declaration); - } - } - } - - protected Set installDerivedNodes(Problem problem) { - var collector = nodeNameCollectorProvider.get(); - collector.collectNodeNames(problem); - Set nodeNames = collector.getNodeNames(); - List graphNodes = problem.getNodes(); - for (String nodeName : nodeNames) { - var graphNode = createNode(nodeName); - graphNodes.add(graphNode); - } - return nodeNames; - } - - protected Node createNode(String name) { - var node = ProblemFactory.eINSTANCE.createNode(); - node.setName(name); - return node; - } - - @Override - public void discardDerivedState(DerivedStateAwareResource resource) { - var problem = getProblem(resource); - if (problem != null) { - var adapter = getOrInstallAdapter(resource); - discardDerivedProblemState(problem, adapter); - } - } - - protected void discardDerivedProblemState(Problem problem, Adapter adapter) { - var abstractClassDeclarations = new HashSet(); - var referenceDeclarationsWithMultiplicity = new HashSet(); - problem.getNodes().clear(); - for (var statement : problem.getStatements()) { - if (statement instanceof ClassDeclaration classDeclaration) { - classDeclaration.setNewNode(null); - if (classDeclaration.isAbstract()) { - abstractClassDeclarations.add(classDeclaration); - } - for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { - if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration && - ProblemUtil.hasMultiplicityConstraint(referenceDeclaration)) { - referenceDeclarationsWithMultiplicity.add(referenceDeclaration); - } - } - } - } - adapter.retainAll(abstractClassDeclarations, referenceDeclarationsWithMultiplicity); - derivedVariableComputer.discardDerivedVariables(problem); - } - - protected Adapter getOrInstallAdapter(Resource resource) { - if (!(resource instanceof XtextResource)) { - return new Adapter(); - } - String resourceLanguageName = ((XtextResource) resource).getLanguageName(); - if (!languageName.equals(resourceLanguageName)) { - return new Adapter(); - } - var adapter = (Adapter) EcoreUtil.getAdapter(resource.eAdapters(), Adapter.class); - if (adapter == null) { - adapter = new Adapter(); - resource.eAdapters().add(adapter); - } - return adapter; - } - - protected static class Adapter extends AdapterImpl { - private final Map newNodes = new HashMap<>(); - private final Map invalidMultiplicityPredicates = new HashMap<>(); - - public Node createNewNodeIfAbsent(ClassDeclaration classDeclaration, - Function createNode) { - return newNodes.computeIfAbsent(classDeclaration, createNode); - } - - public void removeNewNode(ClassDeclaration classDeclaration) { - newNodes.remove(classDeclaration); - } - - public PredicateDefinition createInvalidMultiplicityPredicateIfAbsent( - ReferenceDeclaration referenceDeclaration, - Function createPredicate) { - return invalidMultiplicityPredicates.computeIfAbsent(referenceDeclaration, createPredicate); - } - - public void removeInvalidMultiplicityPredicate(ReferenceDeclaration referenceDeclaration) { - invalidMultiplicityPredicates.remove(referenceDeclaration); - } - - public void retainAll(Collection abstractClassDeclarations, - Collection referenceDeclarationsWithMultiplicity) { - newNodes.keySet().retainAll(abstractClassDeclarations); - invalidMultiplicityPredicates.keySet().retainAll(referenceDeclarationsWithMultiplicity); - } - - @Override - public boolean isAdapterForType(Object type) { - return Adapter.class == type; - } - } -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java index 76fd5852..a2ec7ed0 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java @@ -16,11 +16,13 @@ import org.eclipse.xtext.resource.EObjectDescription; import org.eclipse.xtext.resource.IEObjectDescription; import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; import org.eclipse.xtext.util.IAcceptor; +import tools.refinery.language.scoping.imports.ImportCollector; import tools.refinery.language.model.problem.*; import tools.refinery.language.naming.NamingUtil; import tools.refinery.language.utils.ProblemUtil; import java.util.Map; +import java.util.stream.Collectors; @Singleton public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { @@ -35,12 +37,17 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti public static final String SHADOWING_KEY_RELATION = "relation"; public static final String PREFERRED_NAME = DATA_PREFIX + "PREFERRED_NAME"; public static final String PREFERRED_NAME_TRUE = "true"; + public static final String IMPORTS = DATA_PREFIX + "IMPORTS"; + public static final String IMPORTS_SEPARATOR = "|"; public static final String COLOR_RELATION = DATA_PREFIX + "COLOR_RELATION"; public static final String COLOR_RELATION_TRUE = "true"; @Inject private IQualifiedNameConverter qualifiedNameConverter; + @Inject + private ImportCollector importCollector; + @Override public boolean createEObjectDescriptions(EObject eObject, IAcceptor acceptor) { if (!shouldExport(eObject)) { @@ -115,6 +122,11 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti var builder = ImmutableMap.builder(); if (eObject instanceof Problem) { builder.put(SHADOWING_KEY, SHADOWING_KEY_PROBLEM); + var explicitImports = importCollector.getDirectImports(eObject.eResource()); + var importsString = explicitImports.toList().stream() + .map(importEntry -> importEntry.uri().toString()) + .collect(Collectors.joining(IMPORTS_SEPARATOR)); + builder.put(IMPORTS, importsString); } else if (eObject instanceof Node) { builder.put(SHADOWING_KEY, SHADOWING_KEY_NODE); } else if (eObject instanceof Relation relation) { diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/state/DerivedVariableComputer.java b/subprojects/language/src/main/java/tools/refinery/language/resource/state/DerivedVariableComputer.java new file mode 100644 index 00000000..f0baf35f --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/state/DerivedVariableComputer.java @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.resource.state; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import org.eclipse.xtext.linking.impl.LinkingHelper; +import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.scoping.IScopeProvider; +import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; +import tools.refinery.language.model.problem.*; + +import java.util.*; + +@Singleton +public class DerivedVariableComputer { + @Inject + private LinkingHelper linkingHelper; + + @Inject + private IQualifiedNameConverter qualifiedNameConverter; + + @Inject + @Named(AbstractDeclarativeScopeProvider.NAMED_DELEGATE) + private IScopeProvider scopeProvider; + + public void installDerivedVariables(Problem problem, Set nodeNames) { + for (Statement statement : problem.getStatements()) { + if (statement instanceof ParametricDefinition definition) { + installDerivedParametricDefinitionState(definition, nodeNames); + } + } + } + + protected void installDerivedParametricDefinitionState(ParametricDefinition definition, Set nodeNames) { + Set knownVariables = new HashSet<>(nodeNames); + for (Parameter parameter : definition.getParameters()) { + String name = parameter.getName(); + if (name != null) { + knownVariables.add(name); + } + } + if (definition instanceof PredicateDefinition predicateDefinition) { + installDerivedPredicateDefinitionState(predicateDefinition, knownVariables); + } else if (definition instanceof FunctionDefinition functionDefinition) { + installDerivedFunctionDefinitionState(functionDefinition, knownVariables); + } else if (definition instanceof RuleDefinition ruleDefinition) { + installDerivedRuleDefinitionState(ruleDefinition, knownVariables); + } else { + throw new IllegalArgumentException("Unknown ParametricDefinition: " + definition); + } + } + + protected void installDerivedPredicateDefinitionState(PredicateDefinition definition, Set knownVariables) { + for (Conjunction body : definition.getBodies()) { + createVariablesForScope(new ImplicitVariableScope(body, knownVariables)); + } + } + + protected void installDerivedFunctionDefinitionState(FunctionDefinition definition, Set knownVariables) { + for (Case body : definition.getCases()) { + if (body instanceof Conjunction conjunction) { + createVariablesForScope(new ImplicitVariableScope(conjunction, knownVariables)); + } else if (body instanceof Match match) { + var condition = match.getCondition(); + if (condition != null) { + createVariablesForScope(new ImplicitVariableScope(match, match.getCondition(), knownVariables)); + } + } else { + throw new IllegalArgumentException("Unknown Case: " + body); + } + } + } + + protected void installDerivedRuleDefinitionState(RuleDefinition definition, Set knownVariables) { + for (Conjunction precondition : definition.getPreconditions()) { + createVariablesForScope(new ImplicitVariableScope(precondition, knownVariables)); + } + } + + protected void createVariablesForScope(ImplicitVariableScope scope) { + var queue = new ArrayDeque(); + queue.addLast(scope); + while (!queue.isEmpty()) { + var nextScope = queue.removeFirst(); + nextScope.createVariables(scopeProvider, linkingHelper, qualifiedNameConverter, queue); + } + } + + public void discardDerivedVariables(Problem problem) { + for (Statement statement : problem.getStatements()) { + if (statement instanceof ParametricDefinition parametricDefinition) { + discardParametricDefinitionState(parametricDefinition); + } + } + } + + protected void discardParametricDefinitionState(ParametricDefinition definition) { + List existentialQuantifiers = new ArrayList<>(); + List variableOrNodeExprs = new ArrayList<>(); + var treeIterator = definition.eAllContents(); + // We must collect the nodes where we are discarding derived state and only discard them after the iteration, + // because modifying the containment hierarchy during iteration causes the TreeIterator to fail with + // IndexOutOfBoundsException. + while (treeIterator.hasNext()) { + var child = treeIterator.next(); + var containingFeature = child.eContainingFeature(); + if (containingFeature == ProblemPackage.Literals.EXISTENTIAL_QUANTIFIER__IMPLICIT_VARIABLES || + containingFeature == ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__SINGLETON_VARIABLE) { + treeIterator.prune(); + } else if (child instanceof ExistentialQuantifier existentialQuantifier && + !existentialQuantifier.getImplicitVariables().isEmpty()) { + existentialQuantifiers.add(existentialQuantifier); + } else if (child instanceof VariableOrNodeExpr variableOrNodeExpr && + variableOrNodeExpr.getSingletonVariable() != null) { + variableOrNodeExprs.add(variableOrNodeExpr); + } + } + for (var existentialQuantifier : existentialQuantifiers) { + existentialQuantifier.getImplicitVariables().clear(); + } + for (var variableOrNodeExpr : variableOrNodeExprs) { + variableOrNodeExpr.setSingletonVariable(null); + } + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/state/ImplicitVariableScope.java b/subprojects/language/src/main/java/tools/refinery/language/resource/state/ImplicitVariableScope.java new file mode 100644 index 00000000..e25887ad --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/state/ImplicitVariableScope.java @@ -0,0 +1,143 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.resource.state; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.linking.impl.LinkingHelper; +import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.nodemodel.INode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.scoping.IScope; +import org.eclipse.xtext.scoping.IScopeProvider; +import tools.refinery.language.model.problem.*; +import tools.refinery.language.naming.NamingUtil; + +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ImplicitVariableScope { + private final EObject root; + + private final ExistentialQuantifier quantifier; + + private final ImplicitVariableScope parent; + + private Set knownVariables; + + private ImplicitVariableScope(ExistentialQuantifier quantifier, ImplicitVariableScope parent) { + this.root = quantifier; + this.quantifier = quantifier; + this.parent = parent; + this.knownVariables = null; + } + + public ImplicitVariableScope(EObject root, ExistentialQuantifier quantifier, Set knownVariables) { + this.root = root; + this.quantifier = quantifier; + this.parent = null; + this.knownVariables = new HashSet<>(knownVariables); + } + + public ImplicitVariableScope(ExistentialQuantifier root, Set knownVariables) { + this(root, root, knownVariables); + } + + public void createVariables(IScopeProvider scopeProvider, LinkingHelper linkingHelper, + IQualifiedNameConverter qualifiedNameConverter, + Deque scopeQueue) { + initializeKnownVariables(); + processEObject(root, scopeProvider, linkingHelper, qualifiedNameConverter); + var treeIterator = root.eAllContents(); + while (treeIterator.hasNext()) { + var child = treeIterator.next(); + if (child instanceof ExistentialQuantifier nestedQuantifier) { + scopeQueue.addLast(new ImplicitVariableScope(nestedQuantifier, this)); + treeIterator.prune(); + } else { + processEObject(child, scopeProvider, linkingHelper, qualifiedNameConverter); + } + } + } + + private void initializeKnownVariables() { + boolean hasKnownVariables = knownVariables != null; + boolean hasParent = parent != null; + if ((hasKnownVariables && hasParent) || (!hasKnownVariables && !hasParent)) { + throw new IllegalStateException("Either known variables or parent must be provided, but not both"); + } + if (hasKnownVariables) { + return; + } + if (parent.knownVariables == null) { + throw new IllegalStateException("Parent scope must be processed before current scope"); + } + knownVariables = new HashSet<>(parent.knownVariables); + } + + private void processEObject(EObject eObject, IScopeProvider scopeProvider, LinkingHelper linkingHelper, + IQualifiedNameConverter qualifiedNameConverter) { + if (!(eObject instanceof VariableOrNodeExpr variableOrNodeExpr)) { + return; + } + IScope scope = scopeProvider.getScope(variableOrNodeExpr, + ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE); + List nodes = NodeModelUtils.findNodesForFeature(variableOrNodeExpr, + ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE); + for (INode node : nodes) { + var variableName = linkingHelper.getCrossRefNodeAsString(node, true); + var created = tryCreateVariableForArgument(variableOrNodeExpr, variableName, qualifiedNameConverter, + scope); + if (created) { + break; + } + } + } + + protected boolean tryCreateVariableForArgument(VariableOrNodeExpr variableOrNodeExpr, String variableName, + IQualifiedNameConverter qualifiedNameConverter, IScope scope) { + if (!NamingUtil.isValidId(variableName)) { + return false; + } + QualifiedName qualifiedName; + try { + qualifiedName = qualifiedNameConverter.toQualifiedName(variableName); + } catch (IllegalArgumentException e) { + return false; + } + if (scope.getSingleElement(qualifiedName) != null) { + return false; + } + if (NamingUtil.isSingletonVariableName(variableName)) { + createSingletonVariable(variableOrNodeExpr, variableName); + return true; + } + if (!knownVariables.contains(variableName)) { + createVariable(variableName); + return true; + } + return false; + } + + protected void createVariable(String variableName) { + knownVariables.add(variableName); + ImplicitVariable variable = createNamedVariable(variableName); + quantifier.getImplicitVariables().add(variable); + } + + protected void createSingletonVariable(VariableOrNodeExpr variableOrNodeExpr, String variableName) { + ImplicitVariable variable = createNamedVariable(variableName); + variableOrNodeExpr.setSingletonVariable(variable); + } + + protected ImplicitVariable createNamedVariable(String variableName) { + var variable = ProblemFactory.eINSTANCE.createImplicitVariable(); + variable.setName(variableName); + return variable; + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/state/NodeNameCollector.java b/subprojects/language/src/main/java/tools/refinery/language/resource/state/NodeNameCollector.java new file mode 100644 index 00000000..de4a607c --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/state/NodeNameCollector.java @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.resource.state; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.linking.impl.LinkingHelper; +import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.nodemodel.INode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.scoping.IScope; +import org.eclipse.xtext.scoping.IScopeProvider; +import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; +import tools.refinery.language.model.problem.*; +import tools.refinery.language.naming.NamingUtil; + +import java.util.List; +import java.util.Set; + +public class NodeNameCollector { + @Inject + private LinkingHelper linkingHelper; + + @Inject + private IQualifiedNameConverter qualifiedNameConverter; + + @Inject + @Named(AbstractDeclarativeScopeProvider.NAMED_DELEGATE) + private IScopeProvider scopeProvider; + + private final ImmutableSet.Builder nodeNames = ImmutableSet.builder(); + + private IScope nodeScope; + + public Set getNodeNames() { + return nodeNames.build(); + } + + public void collectNodeNames(Problem problem) { + nodeScope = scopeProvider.getScope(problem, ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE); + for (Statement statement : problem.getStatements()) { + collectStatementNodeNames(statement); + } + } + + protected void collectStatementNodeNames(Statement statement) { + if (statement instanceof Assertion assertion) { + collectAssertionNodeNames(assertion); + } + } + + protected void collectAssertionNodeNames(Assertion assertion) { + for (AssertionArgument argument : assertion.getArguments()) { + if (argument instanceof NodeAssertionArgument) { + collectNodeNames(argument); + } + } + } + + private void collectNodeNames(EObject eObject) { + List nodes = NodeModelUtils.findNodesForFeature(eObject, + ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE); + for (INode node : nodes) { + var nodeName = linkingHelper.getCrossRefNodeAsString(node, true); + if (!NamingUtil.isValidId(nodeName)) { + continue; + } + var qualifiedName = qualifiedNameConverter.toQualifiedName(nodeName); + if (nodeScope.getSingleElement(qualifiedName) == null) { + nodeNames.add(nodeName); + } + } + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/state/ProblemDerivedStateComputer.java b/subprojects/language/src/main/java/tools/refinery/language/resource/state/ProblemDerivedStateComputer.java new file mode 100644 index 00000000..d905aa9a --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/state/ProblemDerivedStateComputer.java @@ -0,0 +1,222 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.resource.state; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import org.eclipse.emf.common.notify.impl.AdapterImpl; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.Constants; +import org.eclipse.xtext.resource.DerivedStateAwareResource; +import org.eclipse.xtext.resource.IDerivedStateComputer; +import org.eclipse.xtext.resource.XtextResource; +import tools.refinery.language.model.problem.*; +import tools.refinery.language.utils.ProblemUtil; + +import java.util.*; +import java.util.function.Function; + +@Singleton +public class ProblemDerivedStateComputer implements IDerivedStateComputer { + public static final String NEW_NODE = "new"; + + @Inject + @Named(Constants.LANGUAGE_NAME) + private String languageName; + + @Inject + private Provider nodeNameCollectorProvider; + + @Inject + private DerivedVariableComputer derivedVariableComputer; + + @Override + public void installDerivedState(DerivedStateAwareResource resource, boolean preLinkingPhase) { + var problem = getProblem(resource); + if (problem != null) { + var adapter = getOrInstallAdapter(resource); + installDerivedProblemState(problem, adapter, preLinkingPhase); + } + } + + protected Problem getProblem(Resource resource) { + List contents = resource.getContents(); + if (contents.isEmpty()) { + return null; + } + EObject object = contents.get(0); + if (object instanceof Problem problem) { + return problem; + } + return null; + } + + protected void installDerivedProblemState(Problem problem, Adapter adapter, boolean preLinkingPhase) { + installDerivedClassDeclarationState(problem, adapter); + if (preLinkingPhase) { + return; + } + Set nodeNames = installDerivedNodes(problem); + derivedVariableComputer.installDerivedVariables(problem, nodeNames); + } + + protected void installDerivedClassDeclarationState(Problem problem, Adapter adapter) { + for (var statement : problem.getStatements()) { + if (statement instanceof ClassDeclaration classDeclaration) { + installOrRemoveNewNode(adapter, classDeclaration); + for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { + if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration) { + installOrRemoveInvalidMultiplicityPredicate(adapter, classDeclaration, referenceDeclaration); + } + } + } + } + } + + protected void installOrRemoveNewNode(Adapter adapter, ClassDeclaration declaration) { + if (declaration.isAbstract()) { + var newNode = declaration.getNewNode(); + if (newNode != null) { + declaration.setNewNode(null); + adapter.removeNewNode(declaration); + } + } else { + if (declaration.getNewNode() == null) { + var newNode = adapter.createNewNodeIfAbsent(declaration, key -> createNode(NEW_NODE)); + declaration.setNewNode(newNode); + } + } + } + + protected void installOrRemoveInvalidMultiplicityPredicate( + Adapter adapter, ClassDeclaration containingClassDeclaration, ReferenceDeclaration declaration) { + if (ProblemUtil.hasMultiplicityConstraint(declaration)) { + if (declaration.getInvalidMultiplicity() == null) { + var invalidMultiplicity = adapter.createInvalidMultiplicityPredicateIfAbsent(declaration, key -> { + var predicate = ProblemFactory.eINSTANCE.createPredicateDefinition(); + predicate.setError(true); + predicate.setName("invalidMultiplicity"); + var parameter = ProblemFactory.eINSTANCE.createParameter(); + parameter.setParameterType(containingClassDeclaration); + parameter.setName("node"); + predicate.getParameters().add(parameter); + return predicate; + }); + declaration.setInvalidMultiplicity(invalidMultiplicity); + } + } else { + var invalidMultiplicity = declaration.getInvalidMultiplicity(); + if (invalidMultiplicity != null) { + declaration.setInvalidMultiplicity(null); + adapter.removeInvalidMultiplicityPredicate(declaration); + } + } + } + + protected Set installDerivedNodes(Problem problem) { + var collector = nodeNameCollectorProvider.get(); + collector.collectNodeNames(problem); + Set nodeNames = collector.getNodeNames(); + List graphNodes = problem.getNodes(); + for (String nodeName : nodeNames) { + var graphNode = createNode(nodeName); + graphNodes.add(graphNode); + } + return nodeNames; + } + + protected Node createNode(String name) { + var node = ProblemFactory.eINSTANCE.createNode(); + node.setName(name); + return node; + } + + @Override + public void discardDerivedState(DerivedStateAwareResource resource) { + var problem = getProblem(resource); + if (problem != null) { + var adapter = getOrInstallAdapter(resource); + discardDerivedProblemState(problem, adapter); + } + } + + protected void discardDerivedProblemState(Problem problem, Adapter adapter) { + var abstractClassDeclarations = new HashSet(); + var referenceDeclarationsWithMultiplicity = new HashSet(); + problem.getNodes().clear(); + for (var statement : problem.getStatements()) { + if (statement instanceof ClassDeclaration classDeclaration) { + classDeclaration.setNewNode(null); + if (classDeclaration.isAbstract()) { + abstractClassDeclarations.add(classDeclaration); + } + for (var featureDeclaration : classDeclaration.getFeatureDeclarations()) { + if (featureDeclaration instanceof ReferenceDeclaration referenceDeclaration && + ProblemUtil.hasMultiplicityConstraint(referenceDeclaration)) { + referenceDeclarationsWithMultiplicity.add(referenceDeclaration); + } + } + } + } + adapter.retainAll(abstractClassDeclarations, referenceDeclarationsWithMultiplicity); + derivedVariableComputer.discardDerivedVariables(problem); + } + + protected Adapter getOrInstallAdapter(Resource resource) { + if (!(resource instanceof XtextResource)) { + return new Adapter(); + } + String resourceLanguageName = ((XtextResource) resource).getLanguageName(); + if (!languageName.equals(resourceLanguageName)) { + return new Adapter(); + } + var adapter = (Adapter) EcoreUtil.getAdapter(resource.eAdapters(), Adapter.class); + if (adapter == null) { + adapter = new Adapter(); + resource.eAdapters().add(adapter); + } + return adapter; + } + + protected static class Adapter extends AdapterImpl { + private final Map newNodes = new HashMap<>(); + private final Map invalidMultiplicityPredicates = new HashMap<>(); + + public Node createNewNodeIfAbsent(ClassDeclaration classDeclaration, + Function createNode) { + return newNodes.computeIfAbsent(classDeclaration, createNode); + } + + public void removeNewNode(ClassDeclaration classDeclaration) { + newNodes.remove(classDeclaration); + } + + public PredicateDefinition createInvalidMultiplicityPredicateIfAbsent( + ReferenceDeclaration referenceDeclaration, + Function createPredicate) { + return invalidMultiplicityPredicates.computeIfAbsent(referenceDeclaration, createPredicate); + } + + public void removeInvalidMultiplicityPredicate(ReferenceDeclaration referenceDeclaration) { + invalidMultiplicityPredicates.remove(referenceDeclaration); + } + + public void retainAll(Collection abstractClassDeclarations, + Collection referenceDeclarationsWithMultiplicity) { + newNodes.keySet().retainAll(abstractClassDeclarations); + invalidMultiplicityPredicates.keySet().retainAll(referenceDeclarationsWithMultiplicity); + } + + @Override + public boolean isAdapterForType(Object type) { + return Adapter.class == type; + } + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/CompositeSelectable.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/CompositeSelectable.java new file mode 100644 index 00000000..0fddcaf9 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/CompositeSelectable.java @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.scoping; + +import com.google.common.collect.Iterables; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.IEObjectDescription; +import org.eclipse.xtext.resource.ISelectable; + +import java.util.Collection; +import java.util.List; + +class CompositeSelectable implements ISelectable { + private static final CompositeSelectable EMPTY = new CompositeSelectable(List.of()); + + private final List children; + + private CompositeSelectable(List children) { + + this.children = children; + } + + @Override + public boolean isEmpty() { + return children.isEmpty(); + } + + @Override + public Iterable getExportedObjects() { + return Iterables.concat(Iterables.transform(children, ISelectable::getExportedObjects)); + } + + @Override + public Iterable getExportedObjects(EClass type, QualifiedName name, boolean ignoreCase) { + return Iterables.concat(Iterables.transform(children, child -> child.getExportedObjects(type, name, + ignoreCase))); + } + + @Override + public Iterable getExportedObjectsByType(EClass type) { + return Iterables.concat(Iterables.transform(children, child -> child.getExportedObjectsByType(type))); + } + + @Override + public Iterable getExportedObjectsByObject(EObject object) { + return Iterables.concat(Iterables.transform(children, child -> child.getExportedObjectsByObject(object))); + } + + public static ISelectable of(Collection children) { + var filteredChildren = children.stream().filter(selectable -> !selectable.isEmpty()).toList(); + return switch (filteredChildren.size()) { + case 0 -> EMPTY; + case 1 -> filteredChildren.getFirst(); + default -> new CompositeSelectable(filteredChildren); + }; + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/NormalizedSelectable.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/NormalizedSelectable.java index 0c7828d8..09fe4716 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/scoping/NormalizedSelectable.java +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/NormalizedSelectable.java @@ -12,18 +12,21 @@ import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.resource.IEObjectDescription; import org.eclipse.xtext.resource.ISelectable; import org.eclipse.xtext.resource.impl.AliasedEObjectDescription; -import org.jetbrains.annotations.NotNull; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; -public class NormalizedSelectable implements ISelectable { +class NormalizedSelectable implements ISelectable { private final ISelectable delegateSelectable; private final QualifiedName originalPrefix; private final QualifiedName normalizedPrefix; - private NormalizedSelectable(ISelectable delegateSelectable, QualifiedName originalPrefix, - QualifiedName normalizedPrefix) { + public NormalizedSelectable(ISelectable delegateSelectable, QualifiedName originalPrefix, + QualifiedName normalizedPrefix) { + if (originalPrefix.equals(QualifiedName.EMPTY)) { + throw new IllegalArgumentException("Cannot normalize empty qualified name prefix"); + } this.delegateSelectable = delegateSelectable; this.originalPrefix = originalPrefix; this.normalizedPrefix = normalizedPrefix; @@ -37,41 +40,36 @@ public class NormalizedSelectable implements ISelectable { @Override public Iterable getExportedObjects() { var delegateIterable = delegateSelectable.getExportedObjects(); - var aliasedIterable = getAliasedElements(delegateIterable); - return Iterables.concat(delegateIterable, aliasedIterable); + return getAliasedElements(delegateIterable); } @Override public Iterable getExportedObjects(EClass type, QualifiedName name, boolean ignoreCase) { - var delegateIterable = delegateSelectable.getExportedObjects(type, name, ignoreCase); boolean startsWith = ignoreCase ? name.startsWithIgnoreCase(normalizedPrefix) : name.startsWith(normalizedPrefix); if (startsWith && name.getSegmentCount() > normalizedPrefix.getSegmentCount()) { var originalName = originalPrefix.append(name.skipFirst(normalizedPrefix.getSegmentCount())); - var originalIterable = Iterables.transform( + return Iterables.transform( delegateSelectable.getExportedObjects(type, originalName, ignoreCase), description -> { var normalizedName = normalizedPrefix.append( description.getName().skipFirst(originalPrefix.getSegmentCount())); return new AliasedEObjectDescription(normalizedName, description); }); - return Iterables.concat(originalIterable, delegateIterable); } - return delegateIterable; + return List.of(); } @Override public Iterable getExportedObjectsByType(EClass type) { var delegateIterable = delegateSelectable.getExportedObjectsByType(type); - var aliasedIterable = getAliasedElements(delegateIterable); - return Iterables.concat(delegateIterable, aliasedIterable); + return getAliasedElements(delegateIterable); } @Override public Iterable getExportedObjectsByObject(EObject object) { var delegateIterable = delegateSelectable.getExportedObjectsByObject(object); - var aliasedIterable = getAliasedElements(delegateIterable); - return Iterables.concat(delegateIterable, aliasedIterable); + return getAliasedElements(delegateIterable); } private Iterable getAliasedElements(Iterable delegateIterable) { @@ -108,15 +106,4 @@ public class NormalizedSelectable implements ISelectable { } }; } - - public static ISelectable of(@NotNull ISelectable delegateSelectable, @NotNull QualifiedName originalPrefix, - @NotNull QualifiedName normalizedPrefix) { - if (originalPrefix.equals(normalizedPrefix)) { - return delegateSelectable; - } - if (originalPrefix.equals(QualifiedName.EMPTY)) { - throw new IllegalArgumentException("Cannot normalize empty qualified name prefix"); - } - return new NormalizedSelectable(delegateSelectable, originalPrefix, normalizedPrefix); - } } diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemGlobalScopeProvider.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemGlobalScopeProvider.java index 37a67c0c..dad4e5d0 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemGlobalScopeProvider.java +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemGlobalScopeProvider.java @@ -6,35 +6,85 @@ package tools.refinery.language.scoping; import com.google.common.base.Predicate; -import org.eclipse.emf.common.util.URI; +import com.google.inject.Inject; +import com.google.inject.Provider; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.resource.IEObjectDescription; -import org.eclipse.xtext.resource.IResourceDescriptions; import org.eclipse.xtext.resource.ISelectable; import org.eclipse.xtext.scoping.IScope; -import org.eclipse.xtext.scoping.impl.ImportUriGlobalScopeProvider; +import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeProvider; import org.eclipse.xtext.scoping.impl.SelectableBasedScope; -import tools.refinery.language.utils.ProblemUtil; +import org.eclipse.xtext.util.IResourceScopeCache; +import tools.refinery.language.scoping.imports.ImportCollector; +import tools.refinery.language.resource.LoadOnDemandResourceDescriptionProvider; +import tools.refinery.language.scoping.imports.NamedImport; -import java.util.LinkedHashSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; -public class ProblemGlobalScopeProvider extends ImportUriGlobalScopeProvider { +public class ProblemGlobalScopeProvider extends AbstractGlobalScopeProvider { + private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemGlobalScopeProvider.CACHE_KEY"; + + @Inject + private ImportCollector importCollector; + + @Inject + private Provider loadOnDemandProvider; + + @Inject + private IResourceScopeCache cache; + + // {@link com.google.common.base.Predicate} required by Xtext API. + @SuppressWarnings("squid:S4738") @Override - protected LinkedHashSet getImportedUris(Resource resource) { - LinkedHashSet importedUris = new LinkedHashSet<>(); - importedUris.add(ProblemUtil.BUILTIN_LIBRARY_URI); - return importedUris; + protected IScope getScope(Resource resource, boolean ignoreCase, EClass type, + Predicate filter) { + var loadedImports = cache.get(CACHE_KEY, resource, () -> computeLoadedImports(resource)); + var qualifiedScope = createScope(IScope.NULLSCOPE, loadedImports.qualifiedImports(), type, filter, ignoreCase); + var implicitScope = createScope(qualifiedScope, loadedImports.implicitImports(), type, filter, ignoreCase); + return createScope(implicitScope, loadedImports.explicitImports(), type, filter, ignoreCase); } - @Override - protected IScope createLazyResourceScope(IScope parent, URI uri, IResourceDescriptions descriptions, EClass type, - Predicate filter, boolean ignoreCase) { - ISelectable description = descriptions.getResourceDescription(uri); - if (description != null && ProblemUtil.BUILTIN_LIBRARY_URI.equals(uri)) { - description = NormalizedSelectable.of(description, QualifiedName.create("builtin"), QualifiedName.EMPTY); + protected LoadedImports computeLoadedImports(Resource resource) { + var imports = importCollector.getAllImports(resource); + var loadOnDemand = loadOnDemandProvider.get(); + loadOnDemand.setContext(resource); + var qualifiedImports = new ArrayList(); + var implicitImports = new ArrayList(); + var explicitImports = new ArrayList(); + for (var importEntry : imports.toList()) { + var uri = importEntry.uri(); + var resourceDescription = loadOnDemand.getResourceDescription(uri); + if (resourceDescription == null) { + continue; + } + qualifiedImports.add(resourceDescription); + if (importEntry instanceof NamedImport namedImport) { + var qualifiedName = namedImport.qualifiedName(); + if (namedImport.alsoImplicit()) { + implicitImports.add(new NormalizedSelectable(resourceDescription, qualifiedName, + QualifiedName.EMPTY)); + } + for (var alias : namedImport.aliases()) { + explicitImports.add(new NormalizedSelectable(resourceDescription, qualifiedName, alias)); + } + } } - return SelectableBasedScope.createScope(parent, description, filter, type, ignoreCase); + return new LoadedImports(qualifiedImports, implicitImports, explicitImports); + } + + // {@link com.google.common.base.Predicate} required by Xtext API. + @SuppressWarnings("squid:S4738") + protected IScope createScope(IScope parent, Collection children, EClass type, + Predicate filter, boolean ignoreCase) { + var selectable = CompositeSelectable.of(children); + return SelectableBasedScope.createScope(parent, selectable, filter, type, ignoreCase); + } + + protected record LoadedImports(List qualifiedImports, List implicitImports, + List explicitImports) { } } diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java index 9be32636..3e00b87e 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java @@ -6,37 +6,72 @@ package tools.refinery.language.scoping; import com.google.inject.Inject; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.naming.IQualifiedNameProvider; import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.resource.IResourceDescription; import org.eclipse.xtext.resource.IResourceDescriptionsProvider; import org.eclipse.xtext.resource.ISelectable; -import org.eclipse.xtext.scoping.impl.SimpleLocalScopeProvider; +import org.eclipse.xtext.scoping.IScope; +import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeDelegatingScopeProvider; +import org.eclipse.xtext.scoping.impl.SelectableBasedScope; +import org.eclipse.xtext.util.IResourceScopeCache; import tools.refinery.language.naming.NamingUtil; -public class ProblemLocalScopeProvider extends SimpleLocalScopeProvider { +public class ProblemLocalScopeProvider extends AbstractGlobalScopeDelegatingScopeProvider { + private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemLocalScopeProvider.CACHE_KEY"; + @Inject private IQualifiedNameProvider qualifiedNameProvider; @Inject private IResourceDescriptionsProvider resourceDescriptionsProvider; + @Inject + private IResourceScopeCache cache; + @Override - protected ISelectable getAllDescriptions(Resource resource) { + public IScope getScope(EObject context, EReference reference) { + var resource = context.eResource(); + if (resource == null) { + return IScope.NULLSCOPE; + } + var localImports = cache.get(CACHE_KEY, resource, () -> computeLocalImports(resource)); + if (localImports.resourceDescription() == null) { + return IScope.NULLSCOPE; + } + var globalScope = getGlobalScope(resource, reference); + var type = reference.getEReferenceType(); + boolean ignoreCase = isIgnoreCase(reference); + var scope = SelectableBasedScope.createScope(globalScope, localImports.resourceDescription(), type, + ignoreCase); + if (localImports.normalizedSelectable() == null) { + return scope; + } + return SelectableBasedScope.createScope(scope, localImports.normalizedSelectable(), type, ignoreCase); + } + + protected LocalImports computeLocalImports(Resource resource) { // Force the use of ProblemResourceDescriptionStrategy to include all QualifiedNames of objects. - var resourceDescriptions = resourceDescriptionsProvider - .getResourceDescriptions(resource.getResourceSet()); + var resourceDescriptions = resourceDescriptionsProvider.getResourceDescriptions(resource.getResourceSet()); var resourceDescription = resourceDescriptions.getResourceDescription(resource.getURI()); - if (resourceDescription != null && !resource.getContents().isEmpty()) { - var rootElement = resource.getContents().getFirst(); - if (rootElement != null) { - var rootName = NamingUtil.stripRootPrefix(qualifiedNameProvider.getFullyQualifiedName(rootElement)); - if (rootName == null) { - return resourceDescription; - } - return NormalizedSelectable.of(resourceDescription, rootName, QualifiedName.EMPTY); - } + if (resourceDescription == null) { + return new LocalImports(null, null); + } + var rootElement = resource.getContents().getFirst(); + if (rootElement == null) { + return new LocalImports(resourceDescription, null); } - return resourceDescription; + var rootName = NamingUtil.stripRootPrefix(qualifiedNameProvider.getFullyQualifiedName(rootElement)); + if (rootName == null) { + return new LocalImports(resourceDescription, null); + } + var normalizedSelectable = new NormalizedSelectable(resourceDescription, rootName, QualifiedName.EMPTY); + return new LocalImports(resourceDescription, normalizedSelectable); + } + + protected record LocalImports(IResourceDescription resourceDescription, ISelectable normalizedSelectable) { } } diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/Import.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/Import.java new file mode 100644 index 00000000..e2f0d3d8 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/Import.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.scoping.imports; + +import org.eclipse.emf.common.util.URI; + +public sealed interface Import permits NamedImport, TransitiveImport { + URI uri(); +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollection.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollection.java new file mode 100644 index 00000000..63171138 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollection.java @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.scoping.imports; + +import org.eclipse.emf.common.util.URI; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class ImportCollection { + public static ImportCollection EMPTY = new ImportCollection() { + @Override + public void add(Import importEntry) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public void remove(URI uri) { + throw new UnsupportedOperationException("Read-only collection"); + } + }; + + private final Map importMap = new HashMap<>(); + + public void add(Import importEntry) { + importMap.compute(importEntry.uri(), (ignored, originalEntry) -> merge(originalEntry, importEntry)); + } + + public void addAll(Iterable imports) { + imports.forEach(this::add); + } + + public void remove(URI uri) { + importMap.remove(uri); + } + + public List toList() { + return List.copyOf(importMap.values()); + } + + public Set toUriSet() { + return new LinkedHashSet<>(importMap.keySet()); + } + + @NotNull + private static Import merge(@Nullable Import left, @NotNull Import right) { + if (left == null) { + return right; + } + if (!left.uri().equals(right.uri())) { + throw new IllegalArgumentException("Expected URIs '%s' and '%s' to be equal".formatted( + left.uri(), right.uri())); + } + return switch (left) { + case TransitiveImport transitiveLeft -> + right instanceof TransitiveImport ? left : merge(right, transitiveLeft); + case NamedImport namedLeft -> switch (right) { + case TransitiveImport ignored -> namedLeft; + case NamedImport namedRight -> { + if (!namedLeft.qualifiedName().equals(namedRight.qualifiedName())) { + throw new IllegalArgumentException("Expected qualified names '%s' and '%s' to be equal" + .formatted(namedLeft.qualifiedName(), namedRight.qualifiedName())); + } + var mergedAliases = new LinkedHashSet<>(namedLeft.aliases()); + mergedAliases.addAll(namedRight.aliases()); + yield new NamedImport(namedLeft.uri(), namedLeft.qualifiedName(), + List.copyOf(mergedAliases), namedLeft.alsoImplicit() || namedRight.alsoImplicit()); + } + }; + }; + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java new file mode 100644 index 00000000..cea99f0a --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java @@ -0,0 +1,138 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.scoping.imports; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.linking.impl.LinkingHelper; +import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.resource.IEObjectDescription; +import org.eclipse.xtext.util.IResourceScopeCache; +import tools.refinery.language.library.RefineryLibraries; +import tools.refinery.language.model.problem.ImportStatement; +import tools.refinery.language.model.problem.Problem; +import tools.refinery.language.model.problem.ProblemPackage; +import tools.refinery.language.naming.NamingUtil; +import tools.refinery.language.resource.LoadOnDemandResourceDescriptionProvider; +import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; + +import java.util.*; + +@Singleton +public class ImportCollector { + private static final String PREFIX = "tools.refinery.language.imports."; + private static final String DIRECT_IMPORTS_KEY = PREFIX + "DIRECT_IMPORTS"; + private static final String ALL_IMPORTS_KEY = PREFIX + "ALL_IMPORTS"; + + @Inject + private IResourceScopeCache cache; + + @Inject + private LinkingHelper linkingHelper; + + @Inject + private IQualifiedNameConverter qualifiedNameConverter; + + @Inject + private Provider loadOnDemandProvider; + + public ImportCollection getDirectImports(Resource resource) { + return cache.get(DIRECT_IMPORTS_KEY, resource, () -> this.computeDirectImports(resource)); + } + + protected ImportCollection computeDirectImports(Resource resource) { + if (resource.getContents().isEmpty() || !(resource.getContents().getFirst() instanceof Problem problem)) { + return ImportCollection.EMPTY; + } + Map> aliasesMap = new LinkedHashMap<>(); + for (var statement : problem.getStatements()) { + if (statement instanceof ImportStatement importStatement) { + collectImportStatement(importStatement, aliasesMap); + } + } + var collection = new ImportCollection(); + collection.addAll(RefineryLibraries.getAutomaticImports()); + for (var entry : aliasesMap.entrySet()) { + var qualifiedName = entry.getKey(); + RefineryLibraries.resolveQualifiedName(qualifiedName).ifPresent(uri -> { + if (!uri.equals(resource.getURI())) { + var aliases = entry.getValue(); + collection.add(NamedImport.explicit(uri, qualifiedName, List.copyOf(aliases))); + } + }); + } + collection.remove(resource.getURI()); + return collection; + } + + private void collectImportStatement(ImportStatement importStatement, Map> aliasesMap) { + var nodes = NodeModelUtils.findNodesForFeature(importStatement, + ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE); + var aliasString = importStatement.getAlias(); + var alias = Strings.isNullOrEmpty(aliasString) ? QualifiedName.EMPTY : + NamingUtil.stripRootPrefix(qualifiedNameConverter.toQualifiedName(aliasString)); + for (var node : nodes) { + var qualifiedNameString = linkingHelper.getCrossRefNodeAsString(node, true); + if (Strings.isNullOrEmpty(qualifiedNameString)) { + continue; + } + var qualifiedName = NamingUtil.stripRootPrefix( + qualifiedNameConverter.toQualifiedName(qualifiedNameString)); + var aliases = aliasesMap.computeIfAbsent(qualifiedName, ignored -> new LinkedHashSet<>()); + aliases.add(alias); + } + } + + public ImportCollection getAllImports(Resource resource) { + return cache.get(ALL_IMPORTS_KEY, resource, () -> this.computeAllImports(resource)); + } + + protected ImportCollection computeAllImports(Resource resource) { + var collection = new ImportCollection(); + collection.addAll(getDirectImports(resource).toList()); + var loadOnDemand = loadOnDemandProvider.get(); + loadOnDemand.setContext(resource); + var seen = new HashSet(); + seen.add(resource.getURI()); + var queue = new ArrayDeque<>(collection.toUriSet()); + while (!queue.isEmpty()) { + var uri = queue.removeFirst(); + seen.add(uri); + collection.add(new TransitiveImport(uri)); + var resourceDescription = loadOnDemand.getResourceDescription(uri); + if (resourceDescription == null) { + continue; + } + var problemDescriptions = resourceDescription.getExportedObjectsByType(ProblemPackage.Literals.PROBLEM); + for (var eObjectDescription : problemDescriptions) { + for (var importedUri : getImports(eObjectDescription)) { + if (!seen.contains(importedUri)) { + queue.addLast(importedUri); + } + } + } + } + collection.remove(resource.getURI()); + return collection; + } + + protected List getImports(IEObjectDescription eObjectDescription) { + var importString = eObjectDescription.getUserData(ProblemResourceDescriptionStrategy.IMPORTS); + if (importString == null) { + return List.of(); + } + return Splitter.on(ProblemResourceDescriptionStrategy.IMPORTS_SEPARATOR).splitToStream(importString) + .map(URI::createURI) + .toList(); + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/NamedImport.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/NamedImport.java new file mode 100644 index 00000000..f5e89605 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/NamedImport.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.scoping.imports; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.naming.QualifiedName; + +import java.util.List; + +public record NamedImport(URI uri, QualifiedName qualifiedName, List aliases, + boolean alsoImplicit) implements Import { + public static NamedImport implicit(URI uri, QualifiedName qualifiedName) { + return new NamedImport(uri, qualifiedName, List.of(), true); + } + + public static NamedImport explicit(URI uri, QualifiedName qualifiedName, List aliases) { + return new NamedImport(uri, qualifiedName, aliases, false); + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/TransitiveImport.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/TransitiveImport.java new file mode 100644 index 00000000..6f5ceaa6 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/TransitiveImport.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.scoping.imports; + +import org.eclipse.emf.common.util.URI; + +public record TransitiveImport(URI uri) implements Import { +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java index 59e26561..0bd1e50b 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -11,6 +11,7 @@ import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.util.IResourceScopeCache; import org.eclipse.xtext.util.Tuples; +import tools.refinery.language.library.BuiltinLibrary; import tools.refinery.language.model.problem.*; import java.util.*; @@ -27,8 +28,8 @@ public class ProblemDesugarer { private Optional doGetBuiltinProblem(Resource resource) { return Optional.ofNullable(resource).map(Resource::getResourceSet) - .map(resourceSet -> resourceSet.getResource(ProblemUtil.BUILTIN_LIBRARY_URI, true)) - .map(Resource::getContents).filter(contents -> !contents.isEmpty()).map(contents -> contents.get(0)) + .map(resourceSet -> resourceSet.getResource(BuiltinLibrary.BUILTIN_LIBRARY_URI, true)) + .map(Resource::getContents).filter(contents -> !contents.isEmpty()).map(List::getFirst) .filter(Problem.class::isInstance).map(Problem.class::cast); } 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 0f87c04b..23ff55e7 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 @@ -5,16 +5,13 @@ */ package tools.refinery.language.utils; -import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.xtext.EcoreUtil2; +import tools.refinery.language.library.BuiltinLibrary; import tools.refinery.language.model.problem.*; public final class ProblemUtil { - public static final String BUILTIN_LIBRARY_NAME = "builtin"; - public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(); - private ProblemUtil() { throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); } @@ -23,7 +20,7 @@ public final class ProblemUtil { if (eObject != null) { var eResource = eObject.eResource(); if (eResource != null) { - return ProblemUtil.BUILTIN_LIBRARY_URI.equals(eResource.getURI()); + return BuiltinLibrary.BUILTIN_LIBRARY_URI.equals(eResource.getURI()); } } return false; @@ -131,13 +128,4 @@ public final class ProblemUtil { var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); return problem != null && problem.getKind() == ModuleKind.MODULE; } - - private static URI getLibraryUri() { - var libraryResource = ProblemUtil.class.getClassLoader() - .getResource("tools/refinery/language/%s.problem".formatted(BUILTIN_LIBRARY_NAME)); - if (libraryResource == null) { - throw new AssertionError("Library '%s' was not found".formatted(BUILTIN_LIBRARY_NAME)); - } - return URI.createURI(libraryResource.toString()); - } } diff --git a/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary b/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary new file mode 100644 index 00000000..bb7e369d --- /dev/null +++ b/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2024 The Refinery Authors +# +# SPDX-License-Identifier: EPL-2.0 +tools.refinery.language.library.BuiltinLibrary diff --git a/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem b/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem deleted file mode 100644 index 022c3167..00000000 --- a/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem +++ /dev/null @@ -1,16 +0,0 @@ -% SPDX-FileCopyrightText: 2021-2023 The Refinery Authors -% -% SPDX-License-Identifier: EPL-2.0 -problem builtin. - -abstract class node. - -pred exists(node). - -pred equals(left, right). - -abstract class contained extends node. - -pred contains(container, contained contained). - -error invalidContainer(contained contained). diff --git a/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery b/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery new file mode 100644 index 00000000..022c3167 --- /dev/null +++ b/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery @@ -0,0 +1,16 @@ +% SPDX-FileCopyrightText: 2021-2023 The Refinery Authors +% +% SPDX-License-Identifier: EPL-2.0 +problem builtin. + +abstract class node. + +pred exists(node). + +pred equals(left, right). + +abstract class contained extends node. + +pred contains(container, contained contained). + +error invalidContainer(contained contained). -- cgit v1.2.3-70-g09d2