diff options
Diffstat (limited to 'subprojects/language/src/main/java/tools/refinery/language')
41 files changed, 1824 insertions, 144 deletions
diff --git a/subprojects/language/src/main/java/tools/refinery/language/GenerateProblem.mwe2 b/subprojects/language/src/main/java/tools/refinery/language/GenerateProblem.mwe2 index 59eba8f7..46ac18fe 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/GenerateProblem.mwe2 +++ b/subprojects/language/src/main/java/tools/refinery/language/GenerateProblem.mwe2 | |||
@@ -51,7 +51,7 @@ Workflow { | |||
51 | 51 | ||
52 | language = StandardLanguage { | 52 | language = StandardLanguage { |
53 | name = 'tools.refinery.language.Problem' | 53 | name = 'tools.refinery.language.Problem' |
54 | fileExtensions = 'problem' | 54 | fileExtensions = 'problem,refinery' |
55 | referencedResource = 'platform:/resource/tools.refinery.refinery-language-model/model/problem.genmodel' | 55 | referencedResource = 'platform:/resource/tools.refinery.refinery-language-model/model/problem.genmodel' |
56 | serializer = { | 56 | serializer = { |
57 | generateStub = false | 57 | generateStub = false |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext index 0fb96954..f0d6c38c 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext +++ b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -9,13 +9,19 @@ import "http://www.eclipse.org/emf/2002/Ecore" as ecore | |||
9 | import "https://refinery.tools/emf/2021/Problem" | 9 | import "https://refinery.tools/emf/2021/Problem" |
10 | 10 | ||
11 | Problem: | 11 | Problem: |
12 | ("problem" name=Identifier ".")? | 12 | (kind=ModuleKind name=QualifiedName? ".")? |
13 | statements+=Statement*; | 13 | statements+=Statement*; |
14 | 14 | ||
15 | enum ModuleKind: | ||
16 | PROBLEM="problem" | MODULE="module"; | ||
17 | |||
15 | Statement: | 18 | Statement: |
16 | Assertion | ClassDeclaration | EnumDeclaration | | 19 | ImportStatement | Assertion | ClassDeclaration | EnumDeclaration | |
17 | PredicateDefinition | /* FunctionDefinition | RuleDefinition | */ | 20 | PredicateDefinition | /* FunctionDefinition | RuleDefinition | */ |
18 | ScopeDeclaration | IndividualDeclaration; | 21 | ScopeDeclaration | NodeDeclaration; |
22 | |||
23 | ImportStatement: | ||
24 | "import" importedModule=[Problem|QualifiedName] ("as" alias=ID)? "."; | ||
19 | 25 | ||
20 | ClassDeclaration: | 26 | ClassDeclaration: |
21 | abstract?="abstract"? "class" | 27 | abstract?="abstract"? "class" |
@@ -252,23 +258,28 @@ RangeMultiplicity: | |||
252 | ExactMultiplicity: | 258 | ExactMultiplicity: |
253 | exactValue=INT; | 259 | exactValue=INT; |
254 | 260 | ||
255 | IndividualDeclaration: | 261 | NodeDeclaration: |
256 | "indiv" nodes+=EnumLiteral ("," nodes+=EnumLiteral)* "."; | 262 | ("declare" | "declare"? kind=NodeKind) |
263 | nodes+=EnumLiteral ("," nodes+=EnumLiteral)* "."; | ||
264 | |||
265 | enum NodeKind: | ||
266 | ATOM="atom" | MULTI="multi"; | ||
257 | 267 | ||
258 | UpperBound returns ecore::EInt: | 268 | UpperBound returns ecore::EInt: |
259 | INT | "*"; | 269 | INT | "*"; |
260 | 270 | ||
261 | QualifiedName hidden(): | 271 | QualifiedName hidden(): |
262 | Identifier ("::" Identifier)*; | 272 | "::"? Identifier (QUALIFIED_NAME_SEPARATOR Identifier)*; |
263 | 273 | ||
264 | NonContainmentQualifiedName hidden(): | 274 | NonContainmentQualifiedName hidden(): |
265 | NonContainmentIdentifier ("::" Identifier)*; | 275 | (NonContainmentIdentifier | "::" Identifier) (QUALIFIED_NAME_SEPARATOR Identifier)*; |
266 | 276 | ||
267 | Identifier: | 277 | Identifier: |
268 | NonContainmentIdentifier | "contains" | "container"; | 278 | NonContainmentIdentifier | "contains" | "container"; |
269 | 279 | ||
270 | NonContainmentIdentifier: | 280 | NonContainmentIdentifier: |
271 | ID | "contained" | "sum" | "prod" | "min" | "max"; | 281 | ID | "atom" | "multi" | "contained" | |
282 | "sum" | "prod" | "min" | "max" | "problem" | "module"; | ||
272 | 283 | ||
273 | Real returns ecore::EDouble: | 284 | Real returns ecore::EDouble: |
274 | EXPONENTIAL | INT "." (INT | EXPONENTIAL); | 285 | EXPONENTIAL | INT "." (INT | EXPONENTIAL); |
@@ -276,6 +287,9 @@ Real returns ecore::EDouble: | |||
276 | terminal TRANSITIVE_CLOSURE: | 287 | terminal TRANSITIVE_CLOSURE: |
277 | "synthetic:TRANSITIVE_CLOSURE"; | 288 | "synthetic:TRANSITIVE_CLOSURE"; |
278 | 289 | ||
290 | terminal QUALIFIED_NAME_SEPARATOR: | ||
291 | "synthetic::QUALIFIED_NAME_SEPARATOR"; | ||
292 | |||
279 | @Override | 293 | @Override |
280 | terminal ID: | 294 | terminal ID: |
281 | ('a'..'z' | 'A'..'Z' | '_') ('a'..'z' | 'A'..'Z' | '_' | '0'..'9')*; | 295 | ('a'..'z' | 'A'..'Z' | '_') ('a'..'z' | 'A'..'Z' | '_' | '0'..'9')*; |
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 00dd3de3..f9a564b0 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java +++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -14,7 +14,10 @@ import com.google.inject.name.Names; | |||
14 | import org.eclipse.xtext.conversion.IValueConverterService; | 14 | import org.eclipse.xtext.conversion.IValueConverterService; |
15 | import org.eclipse.xtext.linking.ILinkingService; | 15 | import org.eclipse.xtext.linking.ILinkingService; |
16 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | 16 | import org.eclipse.xtext.naming.IQualifiedNameConverter; |
17 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | ||
18 | import org.eclipse.xtext.parser.IAstFactory; | ||
17 | import org.eclipse.xtext.parser.IParser; | 19 | import org.eclipse.xtext.parser.IParser; |
20 | import org.eclipse.xtext.parsetree.reconstr.ITransientValueService; | ||
18 | import org.eclipse.xtext.resource.*; | 21 | import org.eclipse.xtext.resource.*; |
19 | import org.eclipse.xtext.scoping.IGlobalScopeProvider; | 22 | import org.eclipse.xtext.scoping.IGlobalScopeProvider; |
20 | import org.eclipse.xtext.scoping.IScopeProvider; | 23 | import org.eclipse.xtext.scoping.IScopeProvider; |
@@ -26,16 +29,21 @@ import org.eclipse.xtext.validation.IResourceValidator; | |||
26 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; | 29 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; |
27 | import tools.refinery.language.conversion.ProblemValueConverterService; | 30 | import tools.refinery.language.conversion.ProblemValueConverterService; |
28 | import tools.refinery.language.linking.ProblemLinkingService; | 31 | import tools.refinery.language.linking.ProblemLinkingService; |
32 | import tools.refinery.language.naming.ProblemDelegateQualifiedNameProvider; | ||
29 | import tools.refinery.language.naming.ProblemQualifiedNameConverter; | 33 | import tools.refinery.language.naming.ProblemQualifiedNameConverter; |
34 | import tools.refinery.language.naming.ProblemQualifiedNameProvider; | ||
35 | import tools.refinery.language.parser.ProblemEcoreElementFactory; | ||
30 | import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; | 36 | import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; |
31 | import tools.refinery.language.resource.ProblemDerivedStateComputer; | ||
32 | import tools.refinery.language.resource.ProblemLocationInFileProvider; | 37 | import tools.refinery.language.resource.ProblemLocationInFileProvider; |
33 | import tools.refinery.language.resource.ProblemResource; | 38 | import tools.refinery.language.resource.ProblemResource; |
39 | import tools.refinery.language.resource.ProblemResourceDescriptionManager; | ||
34 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | 40 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; |
41 | import tools.refinery.language.resource.state.ProblemDerivedStateComputer; | ||
35 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; | 42 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; |
36 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; | 43 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; |
37 | import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; | 44 | import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; |
38 | import tools.refinery.language.serializer.ProblemCrossReferenceSerializer; | 45 | import tools.refinery.language.serializer.ProblemCrossReferenceSerializer; |
46 | import tools.refinery.language.serializer.ProblemTransientValueService; | ||
39 | import tools.refinery.language.validation.ProblemDiagnosticConverter; | 47 | import tools.refinery.language.validation.ProblemDiagnosticConverter; |
40 | 48 | ||
41 | /** | 49 | /** |
@@ -50,10 +58,26 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
50 | return TokenSourceInjectingProblemParser.class; | 58 | return TokenSourceInjectingProblemParser.class; |
51 | } | 59 | } |
52 | 60 | ||
61 | @Override | ||
62 | public Class<? extends IAstFactory> bindIAstFactory() { | ||
63 | return ProblemEcoreElementFactory.class; | ||
64 | } | ||
65 | |||
53 | public Class<? extends IQualifiedNameConverter> bindIQualifiedNameConverter() { | 66 | public Class<? extends IQualifiedNameConverter> bindIQualifiedNameConverter() { |
54 | return ProblemQualifiedNameConverter.class; | 67 | return ProblemQualifiedNameConverter.class; |
55 | } | 68 | } |
56 | 69 | ||
70 | public void configureIQualifiedNameProviderDelegate(Binder binder) { | ||
71 | binder.bind(IQualifiedNameProvider.class) | ||
72 | .annotatedWith(Names.named(ProblemQualifiedNameProvider.NAMED_DELEGATE)) | ||
73 | .to(ProblemDelegateQualifiedNameProvider.class); | ||
74 | } | ||
75 | |||
76 | @Override | ||
77 | public Class<? extends IQualifiedNameProvider> bindIQualifiedNameProvider() { | ||
78 | return ProblemQualifiedNameProvider.class; | ||
79 | } | ||
80 | |||
57 | public Class<? extends IDefaultResourceDescriptionStrategy> bindIDefaultResourceDescriptionStrategy() { | 81 | public Class<? extends IDefaultResourceDescriptionStrategy> bindIDefaultResourceDescriptionStrategy() { |
58 | return ProblemResourceDescriptionStrategy.class; | 82 | return ProblemResourceDescriptionStrategy.class; |
59 | } | 83 | } |
@@ -87,7 +111,7 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
87 | // Method name follows Xtext convention. | 111 | // Method name follows Xtext convention. |
88 | @SuppressWarnings("squid:S100") | 112 | @SuppressWarnings("squid:S100") |
89 | public Class<? extends IResourceDescription.Manager> bindIResourceDescription$Manager() { | 113 | public Class<? extends IResourceDescription.Manager> bindIResourceDescription$Manager() { |
90 | return DerivedStateAwareResourceDescriptionManager.class; | 114 | return ProblemResourceDescriptionManager.class; |
91 | } | 115 | } |
92 | 116 | ||
93 | public Class<? extends IResourceValidator> bindIResourceValidator() { | 117 | public Class<? extends IResourceValidator> bindIResourceValidator() { |
@@ -104,6 +128,11 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
104 | } | 128 | } |
105 | 129 | ||
106 | @Override | 130 | @Override |
131 | public Class<? extends ITransientValueService> bindITransientValueService() { | ||
132 | return ProblemTransientValueService.class; | ||
133 | } | ||
134 | |||
135 | @Override | ||
107 | public Class<? extends ISemanticSequencer> bindISemanticSequencer() { | 136 | public Class<? extends ISemanticSequencer> bindISemanticSequencer() { |
108 | return PreferShortAssertionsProblemSemanticSequencer.class; | 137 | return PreferShortAssertionsProblemSemanticSequencer.class; |
109 | } | 138 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java b/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java index 0f3bd3ee..d6ece1ea 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java +++ b/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java | |||
@@ -23,7 +23,7 @@ public class ProblemFormatter extends AbstractJavaFormatter { | |||
23 | protected void format(Problem problem, IFormattableDocument doc) { | 23 | protected void format(Problem problem, IFormattableDocument doc) { |
24 | doc.prepend(problem, this::noSpace); | 24 | doc.prepend(problem, this::noSpace); |
25 | var region = regionFor(problem); | 25 | var region = regionFor(problem); |
26 | doc.append(region.keyword("problem"), this::oneSpace); | 26 | doc.prepend(region.feature(ProblemPackage.Literals.NAMED_ELEMENT__NAME), this::oneSpace); |
27 | doc.prepend(region.keyword("."), this::noSpace); | 27 | doc.prepend(region.keyword("."), this::noSpace); |
28 | appendNewLines(doc, region.keyword("."), this::twoNewLines); | 28 | appendNewLines(doc, region.keyword("."), this::twoNewLines); |
29 | for (var statement : problem.getStatements()) { | 29 | for (var statement : problem.getStatements()) { |
@@ -132,10 +132,11 @@ public class ProblemFormatter extends AbstractJavaFormatter { | |||
132 | } | 132 | } |
133 | } | 133 | } |
134 | 134 | ||
135 | protected void format(IndividualDeclaration individualDeclaration, IFormattableDocument doc) { | 135 | protected void format(NodeDeclaration nodeDeclaration, IFormattableDocument doc) { |
136 | surroundNewLines(doc, individualDeclaration, this::singleNewLine); | 136 | surroundNewLines(doc, nodeDeclaration, this::singleNewLine); |
137 | var region = regionFor(individualDeclaration); | 137 | var region = regionFor(nodeDeclaration); |
138 | doc.append(region.keyword("indiv"), this::oneSpace); | 138 | doc.append(region.keyword("declare"), this::oneSpace); |
139 | doc.append(region.feature(ProblemPackage.Literals.NODE_DECLARATION__KIND), this::oneSpace); | ||
139 | formatList(region, ",", doc); | 140 | formatList(region, ",", doc); |
140 | doc.prepend(region.keyword("."), this::noSpace); | 141 | doc.prepend(region.keyword("."), this::noSpace); |
141 | } | 142 | } |
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..ce80504e --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java | |||
@@ -0,0 +1,27 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.library; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | ||
10 | |||
11 | import java.util.List; | ||
12 | |||
13 | public class BuiltinLibrary extends ClasspathBasedLibrary { | ||
14 | public static final QualifiedName BUILTIN_LIBRARY_NAME = QualifiedName.create("builtin"); | ||
15 | public static final URI BUILTIN_LIBRARY_URI = ClasspathBasedLibrary.getLibraryUri( | ||
16 | BuiltinLibrary.class, BUILTIN_LIBRARY_NAME).orElseThrow( | ||
17 | () -> new IllegalStateException("Builtin library was not found")); | ||
18 | |||
19 | public BuiltinLibrary() { | ||
20 | super(BUILTIN_LIBRARY_NAME); | ||
21 | } | ||
22 | |||
23 | @Override | ||
24 | public List<QualifiedName> getAutomaticImports() { | ||
25 | return List.of(BUILTIN_LIBRARY_NAME); | ||
26 | } | ||
27 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/ClasspathBasedLibrary.java b/subprojects/language/src/main/java/tools/refinery/language/library/ClasspathBasedLibrary.java new file mode 100644 index 00000000..56bb8b96 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/ClasspathBasedLibrary.java | |||
@@ -0,0 +1,92 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.library; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | ||
10 | |||
11 | import java.nio.file.Path; | ||
12 | import java.util.*; | ||
13 | |||
14 | public abstract class ClasspathBasedLibrary implements RefineryLibrary { | ||
15 | private final Class<?> context; | ||
16 | private final QualifiedName prefix; | ||
17 | private final URI rootUri; | ||
18 | |||
19 | protected ClasspathBasedLibrary(Class<?> context, QualifiedName prefix) { | ||
20 | this.context = context == null ? getClass() : context; | ||
21 | this.prefix = prefix; | ||
22 | var contextPath = this.context.getCanonicalName().replace('.', '/') + ".class"; | ||
23 | var contextResource = this.context.getClassLoader().getResource(contextPath); | ||
24 | if (contextResource == null) { | ||
25 | throw new IllegalStateException("Failed to find library context"); | ||
26 | } | ||
27 | var contextUri = URI.createURI(contextResource.toString()); | ||
28 | var segments = Arrays.copyOf(contextUri.segments(), contextUri.segmentCount() - 1); | ||
29 | rootUri = URI.createHierarchicalURI(contextUri.scheme(), contextUri.authority(), contextUri.device(), | ||
30 | segments, null, null); | ||
31 | } | ||
32 | |||
33 | protected ClasspathBasedLibrary(QualifiedName prefix) { | ||
34 | this(null, prefix); | ||
35 | } | ||
36 | |||
37 | @Override | ||
38 | public Optional<URI> resolveQualifiedName(QualifiedName qualifiedName, List<Path> libraryPaths) { | ||
39 | if (qualifiedName.startsWith(prefix)) { | ||
40 | return getLibraryUri(context, qualifiedName); | ||
41 | } | ||
42 | return Optional.empty(); | ||
43 | } | ||
44 | |||
45 | @Override | ||
46 | public Optional<QualifiedName> getQualifiedName(URI uri, List<Path> libraryPaths) { | ||
47 | if (!uri.isHierarchical() || | ||
48 | !Objects.equals(rootUri.scheme(), uri.scheme()) || | ||
49 | !Objects.equals(rootUri.authority(), uri.authority()) || | ||
50 | !Objects.equals(rootUri.device(), uri.device()) || | ||
51 | rootUri.segmentCount() >= uri.segmentCount()) { | ||
52 | return Optional.empty(); | ||
53 | } | ||
54 | int rootSegmentCount = rootUri.segmentCount(); | ||
55 | int uriSegmentCount = uri.segmentCount(); | ||
56 | if (!uri.segment(uriSegmentCount - 1).endsWith(RefineryLibrary.FILE_NAME_SUFFIX)) { | ||
57 | return Optional.empty(); | ||
58 | } | ||
59 | var segments = new ArrayList<String>(); | ||
60 | int i = 0; | ||
61 | while (i < rootSegmentCount) { | ||
62 | if (!rootUri.segment(i).equals(uri.segment(i))) { | ||
63 | return Optional.empty(); | ||
64 | } | ||
65 | i++; | ||
66 | } | ||
67 | while (i < uriSegmentCount) { | ||
68 | var segment = uri.segment(i); | ||
69 | if (i == uriSegmentCount - 1) { | ||
70 | segment = segment.substring(0, segment.length() - RefineryLibrary.FILE_NAME_SUFFIX.length()); | ||
71 | } | ||
72 | segments.add(segment); | ||
73 | i++; | ||
74 | } | ||
75 | var qualifiedName = QualifiedName.create(segments); | ||
76 | if (!qualifiedName.startsWith(prefix)) { | ||
77 | return Optional.empty(); | ||
78 | } | ||
79 | return Optional.of(qualifiedName); | ||
80 | } | ||
81 | |||
82 | public static Optional<URI> getLibraryUri(Class<?> context, QualifiedName qualifiedName) { | ||
83 | var packagePath = context.getPackageName().replace('.', '/'); | ||
84 | var libraryPath = String.join("/", qualifiedName.getSegments()); | ||
85 | var resourceName = "%s/%s%s".formatted(packagePath, libraryPath, RefineryLibrary.FILE_NAME_SUFFIX); | ||
86 | var resource = context.getClassLoader().getResource(resourceName); | ||
87 | if (resource == null) { | ||
88 | return Optional.empty(); | ||
89 | } | ||
90 | return Optional.of(URI.createURI(resource.toString())); | ||
91 | } | ||
92 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/PathLibrary.java b/subprojects/language/src/main/java/tools/refinery/language/library/PathLibrary.java new file mode 100644 index 00000000..8eaf8458 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/PathLibrary.java | |||
@@ -0,0 +1,90 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.library; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | ||
10 | |||
11 | import java.nio.file.Files; | ||
12 | import java.nio.file.Path; | ||
13 | import java.util.ArrayList; | ||
14 | import java.util.List; | ||
15 | import java.util.Optional; | ||
16 | |||
17 | public final class PathLibrary implements RefineryLibrary { | ||
18 | @Override | ||
19 | public Optional<URI> resolveQualifiedName(QualifiedName qualifiedName, List<Path> libraryPaths) { | ||
20 | if (libraryPaths.isEmpty()) { | ||
21 | return Optional.empty(); | ||
22 | } | ||
23 | if (qualifiedName.getSegmentCount() == 0) { | ||
24 | return Optional.empty(); | ||
25 | } | ||
26 | var relativePath = qualifiedNameToRelativePath(qualifiedName); | ||
27 | for (var library : libraryPaths) { | ||
28 | var absoluteResolvedPath = library.resolve(relativePath).toAbsolutePath().normalize(); | ||
29 | if (absoluteResolvedPath.startsWith(library) && Files.exists(absoluteResolvedPath)) { | ||
30 | var uri = URI.createFileURI(absoluteResolvedPath.toString()); | ||
31 | return Optional.of(uri); | ||
32 | } | ||
33 | } | ||
34 | return Optional.empty(); | ||
35 | } | ||
36 | |||
37 | private static Path qualifiedNameToRelativePath(QualifiedName qualifiedName) { | ||
38 | int segmentCount = qualifiedName.getSegmentCount(); | ||
39 | String first = null; | ||
40 | var rest = new String[segmentCount - 1]; | ||
41 | for (var i = 0; i < segmentCount; i++) { | ||
42 | var segment = qualifiedName.getSegment(i); | ||
43 | if (i == segmentCount - 1) { | ||
44 | segment = segment + RefineryLibrary.FILE_NAME_SUFFIX; | ||
45 | } | ||
46 | if (i == 0) { | ||
47 | first = segment; | ||
48 | } else { | ||
49 | rest[i - 1] = segment; | ||
50 | } | ||
51 | } | ||
52 | if (first == null) { | ||
53 | throw new AssertionError("Expected qualified name to have non-null segments"); | ||
54 | } | ||
55 | return Path.of(first, rest); | ||
56 | } | ||
57 | |||
58 | @Override | ||
59 | public Optional<QualifiedName> getQualifiedName(URI uri, List<Path> libraryPaths) { | ||
60 | if (libraryPaths.isEmpty()) { | ||
61 | return Optional.empty(); | ||
62 | } | ||
63 | if (!uri.isFile() || !uri.hasAbsolutePath()) { | ||
64 | return Optional.empty(); | ||
65 | } | ||
66 | var path = Path.of(uri.toFileString()); | ||
67 | for (var library : libraryPaths) { | ||
68 | if (path.startsWith(library)) { | ||
69 | return getRelativeQualifiedName(library, path); | ||
70 | } | ||
71 | } | ||
72 | return Optional.empty(); | ||
73 | } | ||
74 | |||
75 | private static Optional<QualifiedName> getRelativeQualifiedName(Path library, Path path) { | ||
76 | var relativePath = path.relativize(library); | ||
77 | var segments = new ArrayList<String>(); | ||
78 | for (Path value : relativePath) { | ||
79 | segments.add(value.toString()); | ||
80 | } | ||
81 | int lastIndex = segments.size() - 1; | ||
82 | var lastSegment = segments.get(lastIndex); | ||
83 | if (!lastSegment.endsWith(FILE_NAME_SUFFIX)) { | ||
84 | return Optional.empty(); | ||
85 | } | ||
86 | lastSegment = lastSegment.substring(0, lastSegment.length() - RefineryLibrary.FILE_NAME_SUFFIX.length()); | ||
87 | segments.set(lastIndex, lastSegment); | ||
88 | return Optional.of(QualifiedName.create(segments)); | ||
89 | } | ||
90 | } | ||
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..2559749a --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java | |||
@@ -0,0 +1,26 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.library; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | ||
10 | import tools.refinery.language.utils.ProblemUtil; | ||
11 | |||
12 | import java.nio.file.Path; | ||
13 | import java.util.List; | ||
14 | import java.util.Optional; | ||
15 | |||
16 | public interface RefineryLibrary { | ||
17 | String FILE_NAME_SUFFIX = "." + ProblemUtil.MODULE_EXTENSION; | ||
18 | |||
19 | default List<QualifiedName> getAutomaticImports() { | ||
20 | return List.of(); | ||
21 | } | ||
22 | |||
23 | Optional<URI> resolveQualifiedName(QualifiedName qualifiedName, List<Path> libraryPaths); | ||
24 | |||
25 | Optional<QualifiedName> getQualifiedName(URI uri, List<Path> libraryPaths); | ||
26 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/naming/NamingUtil.java b/subprojects/language/src/main/java/tools/refinery/language/naming/NamingUtil.java index 1647d4e7..feae5ebb 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/naming/NamingUtil.java +++ b/subprojects/language/src/main/java/tools/refinery/language/naming/NamingUtil.java | |||
@@ -1,21 +1,24 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | package tools.refinery.language.naming; | 6 | package tools.refinery.language.naming; |
7 | 7 | ||
8 | import org.eclipse.xtext.naming.QualifiedName; | ||
9 | |||
8 | import java.util.regex.Pattern; | 10 | import java.util.regex.Pattern; |
9 | 11 | ||
10 | public final class NamingUtil { | 12 | public final class NamingUtil { |
11 | private static final String SINGLETON_VARIABLE_PREFIX = "_"; | 13 | private static final String SINGLETON_VARIABLE_PREFIX = "_"; |
12 | 14 | public static final QualifiedName ROOT_NAME = QualifiedName.create(""); | |
13 | private static final Pattern ID_REGEX = Pattern.compile("[_a-zA-Z][_0-9a-zA-Z]*"); | 15 | |
16 | private static final Pattern ID_REGEX = Pattern.compile("[_a-zA-Z]\\w*"); | ||
14 | 17 | ||
15 | private NamingUtil() { | 18 | private NamingUtil() { |
16 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); | 19 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); |
17 | } | 20 | } |
18 | 21 | ||
19 | public static boolean isNullOrEmpty(String name) { | 22 | public static boolean isNullOrEmpty(String name) { |
20 | return name == null || name.isEmpty(); | 23 | return name == null || name.isEmpty(); |
21 | } | 24 | } |
@@ -23,8 +26,28 @@ public final class NamingUtil { | |||
23 | public static boolean isSingletonVariableName(String name) { | 26 | public static boolean isSingletonVariableName(String name) { |
24 | return name != null && name.startsWith(SINGLETON_VARIABLE_PREFIX); | 27 | return name != null && name.startsWith(SINGLETON_VARIABLE_PREFIX); |
25 | } | 28 | } |
26 | 29 | ||
30 | // This method name only makes sense if it checks for the positive case. | ||
31 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||
27 | public static boolean isValidId(String name) { | 32 | public static boolean isValidId(String name) { |
28 | return name != null && ID_REGEX.matcher(name).matches(); | 33 | return name != null && ID_REGEX.matcher(name).matches(); |
29 | } | 34 | } |
35 | |||
36 | public static boolean isFullyQualified(QualifiedName name) { | ||
37 | return name.startsWith(ROOT_NAME); | ||
38 | } | ||
39 | |||
40 | public static QualifiedName stripRootPrefix(QualifiedName name) { | ||
41 | if (name == null) { | ||
42 | return null; | ||
43 | } | ||
44 | return isFullyQualified(name) ? name.skipFirst(ROOT_NAME.getSegmentCount()) : name; | ||
45 | } | ||
46 | |||
47 | public static QualifiedName addRootPrefix(QualifiedName name) { | ||
48 | if (name == null) { | ||
49 | return null; | ||
50 | } | ||
51 | return isFullyQualified(name) ? name : ROOT_NAME.append(name); | ||
52 | } | ||
30 | } | 53 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemDelegateQualifiedNameProvider.java b/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemDelegateQualifiedNameProvider.java new file mode 100644 index 00000000..b3931401 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemDelegateQualifiedNameProvider.java | |||
@@ -0,0 +1,44 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.naming; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.xtext.naming.DefaultDeclarativeQualifiedNameProvider; | ||
10 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
11 | import org.eclipse.xtext.naming.QualifiedName; | ||
12 | import tools.refinery.language.model.problem.Problem; | ||
13 | import tools.refinery.language.scoping.imports.ImportAdapter; | ||
14 | import tools.refinery.language.utils.ProblemUtil; | ||
15 | |||
16 | public class ProblemDelegateQualifiedNameProvider extends DefaultDeclarativeQualifiedNameProvider { | ||
17 | @Inject | ||
18 | private IQualifiedNameConverter qualifiedNameConverter; | ||
19 | |||
20 | protected QualifiedName qualifiedName(Problem problem) { | ||
21 | var qualifiedNameString = problem.getName(); | ||
22 | if (qualifiedNameString != null) { | ||
23 | return NamingUtil.stripRootPrefix(qualifiedNameConverter.toQualifiedName(qualifiedNameString)); | ||
24 | } | ||
25 | if (!ProblemUtil.isModule(problem)) { | ||
26 | return null; | ||
27 | } | ||
28 | var resource = problem.eResource(); | ||
29 | if (resource == null) { | ||
30 | return null; | ||
31 | } | ||
32 | var resourceUri = resource.getURI(); | ||
33 | if (resourceUri == null) { | ||
34 | return null; | ||
35 | } | ||
36 | var resourceSet = resource.getResourceSet(); | ||
37 | if (resourceSet == null) { | ||
38 | return null; | ||
39 | } | ||
40 | var adapter = ImportAdapter.getOrInstall(resourceSet); | ||
41 | // If a module has no explicitly specified name, return the qualified name it was resolved under. | ||
42 | return adapter.getQualifiedName(resourceUri); | ||
43 | } | ||
44 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameConverter.java b/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameConverter.java index 74b4e208..88a0fe9a 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameConverter.java +++ b/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameConverter.java | |||
@@ -12,7 +12,7 @@ import com.google.inject.Singleton; | |||
12 | @Singleton | 12 | @Singleton |
13 | public class ProblemQualifiedNameConverter extends IQualifiedNameConverter.DefaultImpl { | 13 | public class ProblemQualifiedNameConverter extends IQualifiedNameConverter.DefaultImpl { |
14 | public static final String DELIMITER = "::"; | 14 | public static final String DELIMITER = "::"; |
15 | 15 | ||
16 | @Override | 16 | @Override |
17 | public String getDelimiter() { | 17 | public String getDelimiter() { |
18 | return DELIMITER; | 18 | return DELIMITER; |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameProvider.java b/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameProvider.java new file mode 100644 index 00000000..5b682058 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameProvider.java | |||
@@ -0,0 +1,41 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.naming; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.name.Named; | ||
10 | import org.eclipse.emf.ecore.EObject; | ||
11 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | ||
12 | import org.eclipse.xtext.naming.QualifiedName; | ||
13 | import org.eclipse.xtext.util.IResourceScopeCache; | ||
14 | import org.eclipse.xtext.util.Tuples; | ||
15 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | ||
16 | |||
17 | public class ProblemQualifiedNameProvider extends IQualifiedNameProvider.AbstractImpl { | ||
18 | private static final String PREFIX = "tools.refinery.language.naming.ProblemQualifiedNameProvider."; | ||
19 | public static final String NAMED_DELEGATE = PREFIX + "NAMED_DELEGATE"; | ||
20 | public static final String CACHE_KEY = PREFIX + "CACHE_KEY"; | ||
21 | |||
22 | @Inject | ||
23 | @Named(NAMED_DELEGATE) | ||
24 | private IQualifiedNameProvider delegate; | ||
25 | |||
26 | @Inject | ||
27 | private IResourceScopeCache cache = IResourceScopeCache.NullImpl.INSTANCE; | ||
28 | |||
29 | @Override | ||
30 | public QualifiedName getFullyQualifiedName(EObject obj) { | ||
31 | return cache.get(Tuples.pair(obj, CACHE_KEY), obj.eResource(), () -> computeFullyQualifiedName(obj)); | ||
32 | } | ||
33 | |||
34 | public QualifiedName computeFullyQualifiedName(EObject obj) { | ||
35 | var qualifiedName = delegate.getFullyQualifiedName(obj); | ||
36 | if (qualifiedName != null && ProblemResourceDescriptionStrategy.shouldExport(obj)) { | ||
37 | return NamingUtil.addRootPrefix(qualifiedName); | ||
38 | } | ||
39 | return qualifiedName; | ||
40 | } | ||
41 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/parser/ProblemEcoreElementFactory.java b/subprojects/language/src/main/java/tools/refinery/language/parser/ProblemEcoreElementFactory.java new file mode 100644 index 00000000..7d246117 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/parser/ProblemEcoreElementFactory.java | |||
@@ -0,0 +1,24 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.parser; | ||
7 | |||
8 | import org.eclipse.emf.ecore.EObject; | ||
9 | import org.eclipse.xtext.conversion.ValueConverterException; | ||
10 | import org.eclipse.xtext.nodemodel.INode; | ||
11 | import org.eclipse.xtext.parser.DefaultEcoreElementFactory; | ||
12 | import tools.refinery.language.model.problem.Problem; | ||
13 | import tools.refinery.language.model.problem.ProblemPackage; | ||
14 | |||
15 | public class ProblemEcoreElementFactory extends DefaultEcoreElementFactory { | ||
16 | @Override | ||
17 | public void set( | ||
18 | EObject object, String feature, Object value, String ruleName, INode node) throws ValueConverterException { | ||
19 | super.set(object, feature, value, ruleName, node); | ||
20 | if (object instanceof Problem problem && ProblemPackage.Literals.PROBLEM__KIND.getName().equals(feature)) { | ||
21 | problem.setExplicitKind(true); | ||
22 | } | ||
23 | } | ||
24 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/parser/antlr/IdentifierTokenProvider.java b/subprojects/language/src/main/java/tools/refinery/language/parser/antlr/IdentifierTokenProvider.java index 306a86fc..0e19357f 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/parser/antlr/IdentifierTokenProvider.java +++ b/subprojects/language/src/main/java/tools/refinery/language/parser/antlr/IdentifierTokenProvider.java | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -59,7 +59,7 @@ public class IdentifierTokenProvider { | |||
59 | 59 | ||
60 | private void createValueToTokenIdMap() { | 60 | private void createValueToTokenIdMap() { |
61 | var tokenIdToValueMap = tokenDefProvider.getTokenDefMap(); | 61 | var tokenIdToValueMap = tokenDefProvider.getTokenDefMap(); |
62 | valueToTokenIdMap = new HashMap<>(tokenIdToValueMap.size()); | 62 | valueToTokenIdMap = HashMap.newHashMap(tokenIdToValueMap.size()); |
63 | for (var entry : tokenIdToValueMap.entrySet()) { | 63 | for (var entry : tokenIdToValueMap.entrySet()) { |
64 | valueToTokenIdMap.put(entry.getValue(), entry.getKey()); | 64 | valueToTokenIdMap.put(entry.getValue(), entry.getKey()); |
65 | } | 65 | } |
@@ -74,17 +74,16 @@ public class IdentifierTokenProvider { | |||
74 | } | 74 | } |
75 | 75 | ||
76 | private void collectIdentifierTokensFromElement(AbstractElement element) { | 76 | private void collectIdentifierTokensFromElement(AbstractElement element) { |
77 | if (element instanceof Alternatives alternatives) { | 77 | switch (element) { |
78 | for (var alternative : alternatives.getElements()) { | 78 | case Alternatives alternatives -> { |
79 | collectIdentifierTokensFromElement(alternative); | 79 | for (var alternative : alternatives.getElements()) { |
80 | } | 80 | collectIdentifierTokensFromElement(alternative); |
81 | } else if (element instanceof RuleCall ruleCall) { | 81 | } |
82 | collectIdentifierTokensFromRule(ruleCall.getRule()); | 82 | } |
83 | } else if (element instanceof Keyword keyword) { | 83 | case RuleCall ruleCall -> collectIdentifierTokensFromRule(ruleCall.getRule()); |
84 | collectToken("'" + keyword.getValue() + "'"); | 84 | case Keyword keyword -> collectToken("'" + keyword.getValue() + "'"); |
85 | } else { | 85 | default -> throw new IllegalArgumentException("Unknown Xtext grammar element: " + element); |
86 | throw new IllegalArgumentException("Unknown Xtext grammar element: " + element); | 86 | } |
87 | } | ||
88 | } | 87 | } |
89 | 88 | ||
90 | private void collectToken(String value) { | 89 | private void collectToken(String value) { |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/parser/antlr/ProblemTokenSource.java b/subprojects/language/src/main/java/tools/refinery/language/parser/antlr/ProblemTokenSource.java index 5b91a6cc..487e4ceb 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/parser/antlr/ProblemTokenSource.java +++ b/subprojects/language/src/main/java/tools/refinery/language/parser/antlr/ProblemTokenSource.java | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -28,6 +28,8 @@ public class ProblemTokenSource implements TokenSource { | |||
28 | 28 | ||
29 | private boolean seenId; | 29 | private boolean seenId; |
30 | 30 | ||
31 | private boolean lastVisible; | ||
32 | |||
31 | public ProblemTokenSource(TokenSource delegate) { | 33 | public ProblemTokenSource(TokenSource delegate) { |
32 | this.delegate = delegate; | 34 | this.delegate = delegate; |
33 | } | 35 | } |
@@ -47,18 +49,18 @@ public class ProblemTokenSource implements TokenSource { | |||
47 | 49 | ||
48 | @Override | 50 | @Override |
49 | public Token nextToken() { | 51 | public Token nextToken() { |
50 | if (!buffer.isEmpty()) { | 52 | boolean fromStream = buffer.isEmpty(); |
51 | return buffer.removeFirst(); | 53 | var token = fromStream ? delegate.nextToken() : buffer.removeFirst(); |
52 | } | 54 | if (seenId) { |
53 | var token = delegate.nextToken(); | 55 | if (fromStream && isPlusOrTransitiveClosure(token) && peekForTransitiveClosure()) { |
54 | if (isIdentifier(token)) { | ||
55 | seenId = true; | ||
56 | } else if (seenId && isPlusOrTransitiveClosure(token)) { | ||
57 | if (peekForTransitiveClosure()) { | ||
58 | token.setType(InternalProblemParser.RULE_TRANSITIVE_CLOSURE); | 56 | token.setType(InternalProblemParser.RULE_TRANSITIVE_CLOSURE); |
57 | } else if (lastVisible && isQualifiedNameSeparator(token)) { | ||
58 | token.setType(InternalProblemParser.RULE_QUALIFIED_NAME_SEPARATOR); | ||
59 | } | 59 | } |
60 | } else if (isVisibleToken(token)) { | 60 | } |
61 | seenId = false; | 61 | lastVisible = isVisibleToken(token); |
62 | if (lastVisible) { | ||
63 | seenId = isIdentifier(token); | ||
62 | } | 64 | } |
63 | return token; | 65 | return token; |
64 | } | 66 | } |
@@ -76,6 +78,10 @@ public class ProblemTokenSource implements TokenSource { | |||
76 | return token.getType() == InternalProblemParser.PlusSign; | 78 | return token.getType() == InternalProblemParser.PlusSign; |
77 | } | 79 | } |
78 | 80 | ||
81 | protected boolean isQualifiedNameSeparator(Token token) { | ||
82 | return token.getType() == InternalProblemParser.ColonColon; | ||
83 | } | ||
84 | |||
79 | protected boolean isVisibleToken(Token token) { | 85 | protected boolean isVisibleToken(Token token) { |
80 | int tokenId = token.getType(); | 86 | int tokenId = token.getType(); |
81 | return tokenId != InternalProblemParser.RULE_WS && tokenId != InternalProblemParser.RULE_SL_COMMENT && | 87 | return tokenId != InternalProblemParser.RULE_WS && tokenId != InternalProblemParser.RULE_SL_COMMENT && |
@@ -87,11 +93,16 @@ public class ProblemTokenSource implements TokenSource { | |||
87 | if (token.getType() != InternalProblemParser.LeftParenthesis) { | 93 | if (token.getType() != InternalProblemParser.LeftParenthesis) { |
88 | return false; | 94 | return false; |
89 | } | 95 | } |
96 | boolean allowFullyQualifiedName = true; | ||
90 | while (true) { | 97 | while (true) { |
91 | token = peekWithSkipWhitespace(); | 98 | token = peekWithSkipWhitespace(); |
99 | if (allowFullyQualifiedName && token.getType() == InternalProblemParser.ColonColon) { | ||
100 | token = peekWithSkipWhitespace(); | ||
101 | } | ||
92 | if (!isIdentifier(token)) { | 102 | if (!isIdentifier(token)) { |
93 | return false; | 103 | return false; |
94 | } | 104 | } |
105 | allowFullyQualifiedName = false; | ||
95 | token = peekWithSkipWhitespace(); | 106 | token = peekWithSkipWhitespace(); |
96 | switch (token.getType()) { | 107 | switch (token.getType()) { |
97 | case InternalProblemParser.Comma: | 108 | case InternalProblemParser.Comma: |
@@ -112,11 +123,6 @@ public class ProblemTokenSource implements TokenSource { | |||
112 | 123 | ||
113 | protected Token peekToken() { | 124 | protected Token peekToken() { |
114 | var token = delegate.nextToken(); | 125 | var token = delegate.nextToken(); |
115 | if (isIdentifier(token)) { | ||
116 | seenId = true; | ||
117 | } else if (isVisibleToken(token)) { | ||
118 | seenId = false; | ||
119 | } | ||
120 | buffer.addLast(token); | 126 | buffer.addLast(token); |
121 | return token; | 127 | return token; |
122 | } | 128 | } |
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..ccadb42d --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java | |||
@@ -0,0 +1,52 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.resource; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.common.util.URI; | ||
10 | import org.eclipse.emf.ecore.resource.Resource; | ||
11 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
12 | import org.eclipse.xtext.EcoreUtil2; | ||
13 | import org.eclipse.xtext.resource.IResourceDescription; | ||
14 | import org.eclipse.xtext.resource.IResourceDescriptions; | ||
15 | import org.eclipse.xtext.resource.IResourceDescriptionsProvider; | ||
16 | import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider; | ||
17 | |||
18 | public class LoadOnDemandResourceDescriptionProvider { | ||
19 | @Inject | ||
20 | private IResourceDescriptionsProvider resourceDescriptionsProvider; | ||
21 | |||
22 | @Inject | ||
23 | private GlobalResourceDescriptionProvider globalResourceDescriptionProvider; | ||
24 | |||
25 | private Resource context; | ||
26 | private IResourceDescriptions resourceDescriptions; | ||
27 | |||
28 | public void setContext(Resource context) { | ||
29 | if (this.context != null) { | ||
30 | throw new IllegalStateException("Context was already set"); | ||
31 | } | ||
32 | this.context = context; | ||
33 | resourceDescriptions = resourceDescriptionsProvider.getResourceDescriptions(context.getResourceSet()); | ||
34 | } | ||
35 | |||
36 | public IResourceDescription getResourceDescription(URI uri) { | ||
37 | if (this.context == null) { | ||
38 | throw new IllegalStateException("Context was not set"); | ||
39 | } | ||
40 | var resourceDescription = resourceDescriptions.getResourceDescription(uri); | ||
41 | if (resourceDescription != null) { | ||
42 | return resourceDescription; | ||
43 | } | ||
44 | var importedResource = EcoreUtil2.getResource(context, uri.toString()); | ||
45 | if (importedResource == null) { | ||
46 | return null; | ||
47 | } | ||
48 | // Force the {@code importedResource} to have all of its derived resource state installed. | ||
49 | EcoreUtil.resolveAll(importedResource); | ||
50 | return globalResourceDescriptionProvider.getResourceDescription(importedResource); | ||
51 | } | ||
52 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java index 29eaad84..c81431e5 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java | |||
@@ -25,10 +25,10 @@ public class ProblemLocationInFileProvider extends DefaultLocationInFileProvider | |||
25 | } | 25 | } |
26 | 26 | ||
27 | protected ITextRegion getNodeTextRegion(Node node, RegionDescription query) { | 27 | protected ITextRegion getNodeTextRegion(Node node, RegionDescription query) { |
28 | if (ProblemUtil.isIndividualNode(node)) { | 28 | if (ProblemUtil.isDeclaredNode(node)) { |
29 | return super.doGetTextRegion(node, query); | 29 | return super.doGetTextRegion(node, query); |
30 | } | 30 | } |
31 | if (ProblemUtil.isNewNode(node)) { | 31 | if (ProblemUtil.isMultiNode(node)) { |
32 | EObject container = node.eContainer(); | 32 | EObject container = node.eContainer(); |
33 | return doGetTextRegion(container, query); | 33 | return doGetTextRegion(container, query); |
34 | } | 34 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java index 43239ffe..440a238e 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java | |||
@@ -20,9 +20,12 @@ import org.eclipse.xtext.linking.impl.IllegalNodeException; | |||
20 | import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic; | 20 | import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic; |
21 | import org.eclipse.xtext.linking.lazy.LazyLinkingResource; | 21 | import org.eclipse.xtext.linking.lazy.LazyLinkingResource; |
22 | import org.eclipse.xtext.nodemodel.INode; | 22 | import org.eclipse.xtext.nodemodel.INode; |
23 | import org.eclipse.xtext.parser.IParseResult; | ||
23 | import org.eclipse.xtext.resource.DerivedStateAwareResource; | 24 | import org.eclipse.xtext.resource.DerivedStateAwareResource; |
24 | import org.eclipse.xtext.util.Triple; | 25 | import org.eclipse.xtext.util.Triple; |
25 | import org.jetbrains.annotations.Nullable; | 26 | import org.jetbrains.annotations.Nullable; |
27 | import tools.refinery.language.model.problem.Problem; | ||
28 | import tools.refinery.language.utils.ProblemUtil; | ||
26 | 29 | ||
27 | import java.util.Arrays; | 30 | import java.util.Arrays; |
28 | import java.util.List; | 31 | import java.util.List; |
@@ -40,6 +43,30 @@ public class ProblemResource extends DerivedStateAwareResource { | |||
40 | */ | 43 | */ |
41 | private int cyclicLinkingDetectionCounter = 0; | 44 | private int cyclicLinkingDetectionCounter = 0; |
42 | 45 | ||
46 | @Override | ||
47 | protected void updateInternalState(IParseResult oldParseResult, IParseResult newParseResult) { | ||
48 | if (isNewRootElement(oldParseResult, newParseResult) && | ||
49 | newParseResult.getRootASTElement() instanceof Problem newRootProblem && | ||
50 | !newRootProblem.isExplicitKind()) { | ||
51 | // Post-process the parsed model to set its URI-dependent module kind. | ||
52 | // We can't set the default module kind in {@link tools.refinery.language.serializer | ||
53 | // .ProblemTransientValueService}, because the {@link Problem} does not get added into the EMF resource | ||
54 | // before parsing is fully completed. | ||
55 | var defaultModuleKind = ProblemUtil.getDefaultModuleKind(getURI()); | ||
56 | newRootProblem.setKind(defaultModuleKind); | ||
57 | } | ||
58 | super.updateInternalState(oldParseResult, newParseResult); | ||
59 | } | ||
60 | |||
61 | private boolean isNewRootElement(IParseResult oldParseResult, IParseResult newParseResult) { | ||
62 | if (oldParseResult == null) { | ||
63 | return true; | ||
64 | } | ||
65 | var oldRootAstElement = oldParseResult.getRootASTElement(); | ||
66 | var newRootAstElement = newParseResult.getRootASTElement(); | ||
67 | return oldRootAstElement != newRootAstElement; | ||
68 | } | ||
69 | |||
43 | /** | 70 | /** |
44 | * Tries to resolve a reference and emits a diagnostic if the reference is unresolvable or ambiguous. | 71 | * Tries to resolve a reference and emits a diagnostic if the reference is unresolvable or ambiguous. |
45 | * <p> | 72 | * <p> |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescription.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescription.java new file mode 100644 index 00000000..498a7c57 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescription.java | |||
@@ -0,0 +1,106 @@ | |||
1 | /******************************************************************************* | ||
2 | * Copyright (c) 2009, 2011 itemis AG (http://www.itemis.eu) and others. | ||
3 | * Copyright (c) 2024 The Refinery Authors <https://refinery.tools/> | ||
4 | * This program and the accompanying materials are made available under the | ||
5 | * terms of the Eclipse Public License 2.0 which is available at | ||
6 | * http://www.eclipse.org/legal/epl-2.0. | ||
7 | * SPDX-License-Identifier: EPL-2.0 | ||
8 | *******************************************************************************/ | ||
9 | package tools.refinery.language.resource; | ||
10 | |||
11 | import org.apache.log4j.Logger; | ||
12 | import org.eclipse.emf.common.util.TreeIterator; | ||
13 | import org.eclipse.emf.ecore.EObject; | ||
14 | import org.eclipse.emf.ecore.resource.Resource; | ||
15 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
16 | import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy; | ||
17 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
18 | import org.eclipse.xtext.resource.impl.DefaultResourceDescription; | ||
19 | import org.eclipse.xtext.resource.impl.EObjectDescriptionLookUp; | ||
20 | import org.eclipse.xtext.util.IAcceptor; | ||
21 | import tools.refinery.language.naming.NamingUtil; | ||
22 | |||
23 | import java.io.IOException; | ||
24 | import java.util.*; | ||
25 | |||
26 | /** | ||
27 | * A resource description that takes {@link ProblemResourceDescriptionStrategy#SHADOWING_KEY} into account when | ||
28 | * describing EObjects. | ||
29 | * <p> | ||
30 | * Based on {@link DefaultResourceDescription}. | ||
31 | */ | ||
32 | public class ProblemResourceDescription extends DefaultResourceDescription { | ||
33 | private static final Logger log = Logger.getLogger(ProblemResourceDescription.class); | ||
34 | |||
35 | private final IDefaultResourceDescriptionStrategy strategy; | ||
36 | |||
37 | public ProblemResourceDescription(Resource resource, IDefaultResourceDescriptionStrategy strategy) { | ||
38 | super(resource, strategy); | ||
39 | this.strategy = strategy; | ||
40 | } | ||
41 | |||
42 | /** | ||
43 | * Based on {@link DefaultResourceDescription#computeExportedObjects()}. | ||
44 | * | ||
45 | * @return The computed exported objects, taking shadowing into account. | ||
46 | */ | ||
47 | @Override | ||
48 | protected List<IEObjectDescription> computeExportedObjects() { | ||
49 | if (!getResource().isLoaded()) { | ||
50 | try { | ||
51 | getResource().load(null); | ||
52 | } catch (IOException e) { | ||
53 | log.error(e.getMessage(), e); | ||
54 | return Collections.emptyList(); | ||
55 | } | ||
56 | } | ||
57 | final Map<ProblemResourceDescriptionStrategy.ShadowingKey, List<IEObjectDescription>> nameToDescriptionsMap = | ||
58 | new LinkedHashMap<>(); | ||
59 | IAcceptor<IEObjectDescription> acceptor = eObjectDescription -> { | ||
60 | var key = ProblemResourceDescriptionStrategy.getShadowingKey(eObjectDescription); | ||
61 | var descriptions = nameToDescriptionsMap.computeIfAbsent(key, ignored -> new ArrayList<>()); | ||
62 | descriptions.add(eObjectDescription); | ||
63 | }; | ||
64 | TreeIterator<EObject> allProperContents = EcoreUtil.getAllProperContents(getResource(), false); | ||
65 | while (allProperContents.hasNext()) { | ||
66 | EObject content = allProperContents.next(); | ||
67 | if (!strategy.createEObjectDescriptions(content, acceptor)) { | ||
68 | allProperContents.prune(); | ||
69 | } | ||
70 | } | ||
71 | return omitShadowedNames(nameToDescriptionsMap); | ||
72 | } | ||
73 | |||
74 | private static List<IEObjectDescription> omitShadowedNames( | ||
75 | Map<ProblemResourceDescriptionStrategy.ShadowingKey, List<IEObjectDescription>> nameToDescriptionsMap) { | ||
76 | final List<IEObjectDescription> exportedEObjects = new ArrayList<>(); | ||
77 | for (var entry : nameToDescriptionsMap.entrySet()) { | ||
78 | var descriptions = entry.getValue(); | ||
79 | if (NamingUtil.isFullyQualified(entry.getKey().name())) { | ||
80 | exportedEObjects.addAll(descriptions); | ||
81 | } else { | ||
82 | boolean foundPreferred = false; | ||
83 | for (var description : descriptions) { | ||
84 | if (ProblemResourceDescriptionStrategy.PREFERRED_NAME_TRUE.equals( | ||
85 | description.getUserData(ProblemResourceDescriptionStrategy.PREFERRED_NAME))) { | ||
86 | exportedEObjects.add(description); | ||
87 | foundPreferred = true; | ||
88 | } | ||
89 | } | ||
90 | if (!foundPreferred) { | ||
91 | exportedEObjects.addAll(descriptions); | ||
92 | } | ||
93 | } | ||
94 | } | ||
95 | return exportedEObjects; | ||
96 | } | ||
97 | |||
98 | // Based on {@code DerivedStateAwareResourceDescriptionManager#createResourceDescription}. | ||
99 | @Override | ||
100 | protected EObjectDescriptionLookUp getLookUp() { | ||
101 | if (lookup == null) { | ||
102 | lookup = new EObjectDescriptionLookUp(computeExportedObjects()); | ||
103 | } | ||
104 | return lookup; | ||
105 | } | ||
106 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionManager.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionManager.java new file mode 100644 index 00000000..23ca139a --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionManager.java | |||
@@ -0,0 +1,19 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.resource; | ||
7 | |||
8 | import org.eclipse.emf.ecore.resource.Resource; | ||
9 | import org.eclipse.xtext.resource.DerivedStateAwareResourceDescriptionManager; | ||
10 | import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy; | ||
11 | import org.eclipse.xtext.resource.IResourceDescription; | ||
12 | |||
13 | public class ProblemResourceDescriptionManager extends DerivedStateAwareResourceDescriptionManager { | ||
14 | @Override | ||
15 | protected IResourceDescription createResourceDescription(Resource resource, | ||
16 | IDefaultResourceDescriptionStrategy strategy) { | ||
17 | return new ProblemResourceDescription(resource, strategy); | ||
18 | } | ||
19 | } | ||
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 c04c7d09..3080a78e 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 | |||
@@ -8,44 +8,70 @@ package tools.refinery.language.resource; | |||
8 | import com.google.common.collect.ImmutableMap; | 8 | import com.google.common.collect.ImmutableMap; |
9 | import com.google.inject.Inject; | 9 | import com.google.inject.Inject; |
10 | import com.google.inject.Singleton; | 10 | import com.google.inject.Singleton; |
11 | import com.google.inject.name.Named; | ||
11 | import org.eclipse.emf.ecore.EObject; | 12 | import org.eclipse.emf.ecore.EObject; |
12 | import org.eclipse.xtext.EcoreUtil2; | 13 | import org.eclipse.xtext.EcoreUtil2; |
13 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | 14 | import org.eclipse.xtext.naming.IQualifiedNameConverter; |
15 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | ||
14 | import org.eclipse.xtext.naming.QualifiedName; | 16 | import org.eclipse.xtext.naming.QualifiedName; |
15 | import org.eclipse.xtext.resource.EObjectDescription; | 17 | import org.eclipse.xtext.resource.EObjectDescription; |
16 | import org.eclipse.xtext.resource.IEObjectDescription; | 18 | import org.eclipse.xtext.resource.IEObjectDescription; |
17 | import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; | 19 | import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; |
18 | import org.eclipse.xtext.util.IAcceptor; | 20 | import org.eclipse.xtext.util.IAcceptor; |
21 | import tools.refinery.language.naming.ProblemQualifiedNameProvider; | ||
22 | import tools.refinery.language.scoping.imports.ImportCollector; | ||
19 | import tools.refinery.language.model.problem.*; | 23 | import tools.refinery.language.model.problem.*; |
20 | import tools.refinery.language.naming.NamingUtil; | 24 | import tools.refinery.language.naming.NamingUtil; |
21 | import tools.refinery.language.utils.ProblemUtil; | 25 | import tools.refinery.language.utils.ProblemUtil; |
22 | 26 | ||
23 | import java.util.Map; | 27 | import java.util.Map; |
28 | import java.util.stream.Collectors; | ||
24 | 29 | ||
25 | @Singleton | 30 | @Singleton |
26 | public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { | 31 | public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { |
27 | private static final String DATA_PREFIX = "tools.refinery.language.resource.ProblemResourceDescriptionStrategy."; | 32 | private static final String DATA_PREFIX = "tools.refinery.language.resource.ProblemResourceDescriptionStrategy."; |
33 | |||
28 | public static final String ARITY = DATA_PREFIX + "ARITY"; | 34 | public static final String ARITY = DATA_PREFIX + "ARITY"; |
29 | public static final String ERROR_PREDICATE = DATA_PREFIX + "ERROR_PREDICATE"; | 35 | public static final String ERROR_PREDICATE = DATA_PREFIX + "ERROR_PREDICATE"; |
30 | public static final String ERROR_PREDICATE_TRUE = "true"; | 36 | public static final String ERROR_PREDICATE_TRUE = "true"; |
37 | public static final String SHADOWING_KEY = DATA_PREFIX + "SHADOWING_KEY"; | ||
38 | public static final String SHADOWING_KEY_PROBLEM = "problem"; | ||
39 | public static final String SHADOWING_KEY_NODE = "node"; | ||
40 | public static final String SHADOWING_KEY_RELATION = "relation"; | ||
41 | public static final String PREFERRED_NAME = DATA_PREFIX + "PREFERRED_NAME"; | ||
42 | public static final String PREFERRED_NAME_TRUE = "true"; | ||
43 | public static final String IMPORTS = DATA_PREFIX + "IMPORTS"; | ||
44 | public static final String IMPORTS_SEPARATOR = "|"; | ||
45 | public static final String MODULE_KIND = DATA_PREFIX + "MODULE_KIND"; | ||
31 | public static final String COLOR_RELATION = DATA_PREFIX + "COLOR_RELATION"; | 46 | public static final String COLOR_RELATION = DATA_PREFIX + "COLOR_RELATION"; |
32 | public static final String COLOR_RELATION_TRUE = "true"; | 47 | public static final String COLOR_RELATION_TRUE = "true"; |
33 | 48 | ||
34 | @Inject | 49 | @Inject |
35 | private IQualifiedNameConverter qualifiedNameConverter; | 50 | private IQualifiedNameConverter qualifiedNameConverter; |
36 | 51 | ||
52 | @Inject | ||
53 | @Named(ProblemQualifiedNameProvider.NAMED_DELEGATE) | ||
54 | private IQualifiedNameProvider delegateQualifiedNameProvider; | ||
55 | |||
56 | @Inject | ||
57 | private ImportCollector importCollector; | ||
58 | |||
37 | @Override | 59 | @Override |
38 | public boolean createEObjectDescriptions(EObject eObject, IAcceptor<IEObjectDescription> acceptor) { | 60 | public boolean createEObjectDescriptions(EObject eObject, IAcceptor<IEObjectDescription> acceptor) { |
39 | if (!shouldExport(eObject)) { | 61 | if (!shouldExport(eObject)) { |
40 | return false; | 62 | return false; |
41 | } | 63 | } |
64 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); | ||
65 | var problemQualifiedName = getProblemQualifiedName(problem); | ||
66 | var userData = getUserData(eObject); | ||
67 | if (eObject.equals(problem)) { | ||
68 | acceptEObjectDescription(eObject, problemQualifiedName, QualifiedName.EMPTY, userData, true, acceptor); | ||
69 | return true; | ||
70 | } | ||
42 | var qualifiedName = getNameAsQualifiedName(eObject); | 71 | var qualifiedName = getNameAsQualifiedName(eObject); |
43 | if (qualifiedName == null) { | 72 | if (qualifiedName == null) { |
44 | return true; | 73 | return true; |
45 | } | 74 | } |
46 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); | ||
47 | var problemQualifiedName = getNameAsQualifiedName(problem); | ||
48 | var userData = getUserData(eObject); | ||
49 | QualifiedName lastQualifiedNameToExport = null; | 75 | QualifiedName lastQualifiedNameToExport = null; |
50 | if (shouldExportSimpleName(eObject)) { | 76 | if (shouldExportSimpleName(eObject)) { |
51 | lastQualifiedNameToExport = qualifiedName; | 77 | lastQualifiedNameToExport = qualifiedName; |
@@ -82,24 +108,46 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
82 | if (NamingUtil.isNullOrEmpty(name)) { | 108 | if (NamingUtil.isNullOrEmpty(name)) { |
83 | return null; | 109 | return null; |
84 | } | 110 | } |
85 | return qualifiedNameConverter.toQualifiedName(name); | 111 | var qualifiedName = qualifiedNameConverter.toQualifiedName(name); |
112 | if (eObject instanceof Problem) { | ||
113 | return NamingUtil.stripRootPrefix(qualifiedName); | ||
114 | } | ||
115 | return qualifiedName; | ||
86 | } | 116 | } |
87 | 117 | ||
88 | protected boolean shouldExport(EObject eObject) { | 118 | protected QualifiedName getProblemQualifiedName(Problem problem) { |
119 | if (problem == null) { | ||
120 | return QualifiedName.EMPTY; | ||
121 | } | ||
122 | var qualifiedName = delegateQualifiedNameProvider.getFullyQualifiedName(problem); | ||
123 | return qualifiedName == null ? QualifiedName.EMPTY : qualifiedName; | ||
124 | } | ||
125 | |||
126 | public static boolean shouldExport(EObject eObject) { | ||
89 | if (eObject instanceof Variable) { | 127 | if (eObject instanceof Variable) { |
90 | // Variables are always private to the containing predicate definition. | 128 | // Variables are always private to the containing predicate definition. |
91 | return false; | 129 | return false; |
92 | } | 130 | } |
93 | if (eObject instanceof Node node) { | 131 | if (eObject instanceof Node node) { |
94 | // Only enum literals and new nodes are visible across problem files. | 132 | return !ProblemUtil.isImplicitNode(node); |
95 | return ProblemUtil.isIndividualNode(node) || ProblemUtil.isNewNode(node); | ||
96 | } | 133 | } |
97 | return true; | 134 | return true; |
98 | } | 135 | } |
99 | 136 | ||
100 | protected Map<String, String> getUserData(EObject eObject) { | 137 | protected Map<String, String> getUserData(EObject eObject) { |
101 | var builder = ImmutableMap.<String, String>builder(); | 138 | var builder = ImmutableMap.<String, String>builder(); |
102 | if (eObject instanceof Relation relation) { | 139 | if (eObject instanceof Problem problem) { |
140 | builder.put(SHADOWING_KEY, SHADOWING_KEY_PROBLEM); | ||
141 | var explicitImports = importCollector.getDirectImports(eObject.eResource()); | ||
142 | var importsString = explicitImports.toList().stream() | ||
143 | .map(importEntry -> importEntry.uri().toString()) | ||
144 | .collect(Collectors.joining(IMPORTS_SEPARATOR)); | ||
145 | builder.put(IMPORTS, importsString); | ||
146 | builder.put(MODULE_KIND, problem.getKind().getName()); | ||
147 | } else if (eObject instanceof Node) { | ||
148 | builder.put(SHADOWING_KEY, SHADOWING_KEY_NODE); | ||
149 | } else if (eObject instanceof Relation relation) { | ||
150 | builder.put(SHADOWING_KEY, SHADOWING_KEY_RELATION); | ||
103 | int arity = ProblemUtil.getArity(relation); | 151 | int arity = ProblemUtil.getArity(relation); |
104 | builder.put(ARITY, Integer.toString(arity)); | 152 | builder.put(ARITY, Integer.toString(arity)); |
105 | } | 153 | } |
@@ -111,7 +159,7 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
111 | 159 | ||
112 | protected boolean shouldExportSimpleName(EObject eObject) { | 160 | protected boolean shouldExportSimpleName(EObject eObject) { |
113 | if (eObject instanceof Node node) { | 161 | if (eObject instanceof Node node) { |
114 | return !ProblemUtil.isNewNode(node); | 162 | return !ProblemUtil.isMultiNode(node); |
115 | } | 163 | } |
116 | if (eObject instanceof PredicateDefinition predicateDefinition) { | 164 | if (eObject instanceof PredicateDefinition predicateDefinition) { |
117 | return !ProblemUtil.isInvalidMultiplicityConstraint(predicateDefinition); | 165 | return !ProblemUtil.isInvalidMultiplicityConstraint(predicateDefinition); |
@@ -125,20 +173,31 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
125 | } | 173 | } |
126 | 174 | ||
127 | private void acceptEObjectDescription(EObject eObject, QualifiedName prefix, QualifiedName qualifiedName, | 175 | private void acceptEObjectDescription(EObject eObject, QualifiedName prefix, QualifiedName qualifiedName, |
128 | Map<String, String> userData, boolean fullyQualified, | 176 | Map<String, String> userData, boolean preferredName, |
129 | IAcceptor<IEObjectDescription> acceptor) { | 177 | IAcceptor<IEObjectDescription> acceptor) { |
130 | var qualifiedNameWithPrefix = prefix == null ? qualifiedName : prefix.append(qualifiedName); | 178 | var qualifiedNameWithPrefix = prefix == null ? qualifiedName : prefix.append(qualifiedName); |
131 | Map<String, String> userDataWithFullyQualified; | 179 | var userDataWithPreference = userData; |
132 | if (fullyQualified && shouldColorRelation(eObject)) { | 180 | if (preferredName) { |
133 | userDataWithFullyQualified = ImmutableMap.<String, String>builder() | 181 | userDataWithPreference = ImmutableMap.<String, String>builder() |
134 | .putAll(userData) | 182 | .putAll(userData) |
135 | .put(COLOR_RELATION, COLOR_RELATION_TRUE) | 183 | .put(PREFERRED_NAME, PREFERRED_NAME_TRUE) |
136 | .build(); | 184 | .build(); |
137 | } else { | ||
138 | userDataWithFullyQualified = userData; | ||
139 | } | 185 | } |
140 | var description = EObjectDescription.create(qualifiedNameWithPrefix, eObject, userDataWithFullyQualified); | 186 | var description = EObjectDescription.create(qualifiedNameWithPrefix, eObject, userDataWithPreference); |
141 | acceptor.accept(description); | 187 | acceptor.accept(description); |
188 | if (!preferredName) { | ||
189 | return; | ||
190 | } | ||
191 | var userDataWithFullyQualified = userDataWithPreference; | ||
192 | if (shouldColorRelation(eObject)) { | ||
193 | userDataWithFullyQualified = ImmutableMap.<String, String>builder() | ||
194 | .putAll(userDataWithPreference) | ||
195 | .put(COLOR_RELATION, COLOR_RELATION_TRUE) | ||
196 | .build(); | ||
197 | } | ||
198 | var rootQualifiedName = NamingUtil.addRootPrefix(qualifiedNameWithPrefix); | ||
199 | var rootDescription = EObjectDescription.create(rootQualifiedName, eObject, userDataWithFullyQualified); | ||
200 | acceptor.accept(rootDescription); | ||
142 | } | 201 | } |
143 | 202 | ||
144 | private boolean shouldColorRelation(EObject eObject) { | 203 | private boolean shouldColorRelation(EObject eObject) { |
@@ -146,6 +205,12 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
146 | return false; | 205 | return false; |
147 | } | 206 | } |
148 | return eObject instanceof ClassDeclaration || eObject instanceof EnumDeclaration; | 207 | return eObject instanceof ClassDeclaration || eObject instanceof EnumDeclaration; |
208 | } | ||
209 | |||
210 | public static ShadowingKey getShadowingKey(IEObjectDescription description) { | ||
211 | return new ShadowingKey(description.getName(), description.getUserData(SHADOWING_KEY)); | ||
212 | } | ||
149 | 213 | ||
214 | public record ShadowingKey(QualifiedName name, String shadowingKey) { | ||
150 | } | 215 | } |
151 | } | 216 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/DerivedVariableComputer.java b/subprojects/language/src/main/java/tools/refinery/language/resource/state/DerivedVariableComputer.java index 07c5da41..f0baf35f 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/DerivedVariableComputer.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/state/DerivedVariableComputer.java | |||
@@ -3,7 +3,7 @@ | |||
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | package tools.refinery.language.resource; | 6 | package tools.refinery.language.resource.state; |
7 | 7 | ||
8 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
9 | import com.google.inject.Singleton; | 9 | import com.google.inject.Singleton; |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ImplicitVariableScope.java b/subprojects/language/src/main/java/tools/refinery/language/resource/state/ImplicitVariableScope.java index e97c8287..e25887ad 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ImplicitVariableScope.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/state/ImplicitVariableScope.java | |||
@@ -3,7 +3,7 @@ | |||
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | package tools.refinery.language.resource; | 6 | package tools.refinery.language.resource.state; |
7 | 7 | ||
8 | import org.eclipse.emf.ecore.EObject; | 8 | import org.eclipse.emf.ecore.EObject; |
9 | import org.eclipse.xtext.linking.impl.LinkingHelper; | 9 | import org.eclipse.xtext.linking.impl.LinkingHelper; |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/NodeNameCollector.java b/subprojects/language/src/main/java/tools/refinery/language/resource/state/NodeNameCollector.java index e5deca4d..de4a607c 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/NodeNameCollector.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/state/NodeNameCollector.java | |||
@@ -3,7 +3,7 @@ | |||
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | package tools.refinery.language.resource; | 6 | package tools.refinery.language.resource.state; |
7 | 7 | ||
8 | import com.google.common.collect.ImmutableSet; | 8 | import com.google.common.collect.ImmutableSet; |
9 | import com.google.inject.Inject; | 9 | import com.google.inject.Inject; |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java b/subprojects/language/src/main/java/tools/refinery/language/resource/state/ProblemDerivedStateComputer.java index 31eb55a6..d905aa9a 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/state/ProblemDerivedStateComputer.java | |||
@@ -3,7 +3,7 @@ | |||
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | package tools.refinery.language.resource; | 6 | package tools.refinery.language.resource.state; |
7 | 7 | ||
8 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
9 | import com.google.inject.Provider; | 9 | import com.google.inject.Provider; |
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping; | ||
7 | |||
8 | import com.google.common.collect.Iterables; | ||
9 | import org.eclipse.emf.ecore.EClass; | ||
10 | import org.eclipse.emf.ecore.EObject; | ||
11 | import org.eclipse.xtext.naming.QualifiedName; | ||
12 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
13 | import org.eclipse.xtext.resource.ISelectable; | ||
14 | |||
15 | import java.util.Collection; | ||
16 | import java.util.List; | ||
17 | |||
18 | class CompositeSelectable implements ISelectable { | ||
19 | private static final CompositeSelectable EMPTY = new CompositeSelectable(List.of()); | ||
20 | |||
21 | private final List<? extends ISelectable> children; | ||
22 | |||
23 | private CompositeSelectable(List<? extends ISelectable> children) { | ||
24 | |||
25 | this.children = children; | ||
26 | } | ||
27 | |||
28 | @Override | ||
29 | public boolean isEmpty() { | ||
30 | return children.isEmpty(); | ||
31 | } | ||
32 | |||
33 | @Override | ||
34 | public Iterable<IEObjectDescription> getExportedObjects() { | ||
35 | return Iterables.concat(Iterables.transform(children, ISelectable::getExportedObjects)); | ||
36 | } | ||
37 | |||
38 | @Override | ||
39 | public Iterable<IEObjectDescription> getExportedObjects(EClass type, QualifiedName name, boolean ignoreCase) { | ||
40 | return Iterables.concat(Iterables.transform(children, child -> child.getExportedObjects(type, name, | ||
41 | ignoreCase))); | ||
42 | } | ||
43 | |||
44 | @Override | ||
45 | public Iterable<IEObjectDescription> getExportedObjectsByType(EClass type) { | ||
46 | return Iterables.concat(Iterables.transform(children, child -> child.getExportedObjectsByType(type))); | ||
47 | } | ||
48 | |||
49 | @Override | ||
50 | public Iterable<IEObjectDescription> getExportedObjectsByObject(EObject object) { | ||
51 | return Iterables.concat(Iterables.transform(children, child -> child.getExportedObjectsByObject(object))); | ||
52 | } | ||
53 | |||
54 | public static ISelectable of(Collection<? extends ISelectable> children) { | ||
55 | var filteredChildren = children.stream().filter(selectable -> !selectable.isEmpty()).toList(); | ||
56 | return switch (filteredChildren.size()) { | ||
57 | case 0 -> EMPTY; | ||
58 | case 1 -> filteredChildren.getFirst(); | ||
59 | default -> new CompositeSelectable(filteredChildren); | ||
60 | }; | ||
61 | } | ||
62 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/NoFullyQualifiedNamesSelectable.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/NoFullyQualifiedNamesSelectable.java new file mode 100644 index 00000000..f9405fc1 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/NoFullyQualifiedNamesSelectable.java | |||
@@ -0,0 +1,62 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping; | ||
7 | |||
8 | import com.google.common.base.Predicate; | ||
9 | import com.google.common.collect.Iterables; | ||
10 | import org.eclipse.emf.ecore.EClass; | ||
11 | import org.eclipse.emf.ecore.EObject; | ||
12 | import org.eclipse.xtext.naming.QualifiedName; | ||
13 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
14 | import org.eclipse.xtext.resource.ISelectable; | ||
15 | import tools.refinery.language.naming.NamingUtil; | ||
16 | |||
17 | import java.util.List; | ||
18 | |||
19 | public class NoFullyQualifiedNamesSelectable implements ISelectable { | ||
20 | private final ISelectable delegateSelectable; | ||
21 | |||
22 | // {@link com.google.common.base.Predicate} required by Xtext API. | ||
23 | @SuppressWarnings("squid:S4738") | ||
24 | private final Predicate<IEObjectDescription> filter = | ||
25 | eObjectDescription -> !NamingUtil.isFullyQualified(eObjectDescription.getName()); | ||
26 | |||
27 | public NoFullyQualifiedNamesSelectable(ISelectable delegateSelectable) { | ||
28 | this.delegateSelectable = delegateSelectable; | ||
29 | } | ||
30 | |||
31 | @Override | ||
32 | public boolean isEmpty() { | ||
33 | return delegateSelectable.isEmpty(); | ||
34 | } | ||
35 | |||
36 | @Override | ||
37 | public Iterable<IEObjectDescription> getExportedObjects() { | ||
38 | return filter(delegateSelectable.getExportedObjects()); | ||
39 | } | ||
40 | |||
41 | @Override | ||
42 | public Iterable<IEObjectDescription> getExportedObjects(EClass type, QualifiedName name, boolean ignoreCase) { | ||
43 | if (NamingUtil.isFullyQualified(name)) { | ||
44 | return List.of(); | ||
45 | } | ||
46 | return delegateSelectable.getExportedObjects(type, name, ignoreCase); | ||
47 | } | ||
48 | |||
49 | @Override | ||
50 | public Iterable<IEObjectDescription> getExportedObjectsByType(EClass type) { | ||
51 | return filter(delegateSelectable.getExportedObjectsByType(type)); | ||
52 | } | ||
53 | |||
54 | @Override | ||
55 | public Iterable<IEObjectDescription> getExportedObjectsByObject(EObject object) { | ||
56 | return filter(delegateSelectable.getExportedObjectsByObject(object)); | ||
57 | } | ||
58 | |||
59 | private Iterable<IEObjectDescription> filter(Iterable<IEObjectDescription> eObjectDescriptions) { | ||
60 | return Iterables.filter(eObjectDescriptions, filter); | ||
61 | } | ||
62 | } | ||
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 new file mode 100644 index 00000000..09fe4716 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/NormalizedSelectable.java | |||
@@ -0,0 +1,109 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping; | ||
7 | |||
8 | import com.google.common.collect.Iterables; | ||
9 | import org.eclipse.emf.ecore.EClass; | ||
10 | import org.eclipse.emf.ecore.EObject; | ||
11 | import org.eclipse.xtext.naming.QualifiedName; | ||
12 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
13 | import org.eclipse.xtext.resource.ISelectable; | ||
14 | import org.eclipse.xtext.resource.impl.AliasedEObjectDescription; | ||
15 | |||
16 | import java.util.Iterator; | ||
17 | import java.util.List; | ||
18 | import java.util.NoSuchElementException; | ||
19 | |||
20 | class NormalizedSelectable implements ISelectable { | ||
21 | private final ISelectable delegateSelectable; | ||
22 | private final QualifiedName originalPrefix; | ||
23 | private final QualifiedName normalizedPrefix; | ||
24 | |||
25 | public NormalizedSelectable(ISelectable delegateSelectable, QualifiedName originalPrefix, | ||
26 | QualifiedName normalizedPrefix) { | ||
27 | if (originalPrefix.equals(QualifiedName.EMPTY)) { | ||
28 | throw new IllegalArgumentException("Cannot normalize empty qualified name prefix"); | ||
29 | } | ||
30 | this.delegateSelectable = delegateSelectable; | ||
31 | this.originalPrefix = originalPrefix; | ||
32 | this.normalizedPrefix = normalizedPrefix; | ||
33 | } | ||
34 | |||
35 | @Override | ||
36 | public boolean isEmpty() { | ||
37 | return delegateSelectable.isEmpty(); | ||
38 | } | ||
39 | |||
40 | @Override | ||
41 | public Iterable<IEObjectDescription> getExportedObjects() { | ||
42 | var delegateIterable = delegateSelectable.getExportedObjects(); | ||
43 | return getAliasedElements(delegateIterable); | ||
44 | } | ||
45 | |||
46 | @Override | ||
47 | public Iterable<IEObjectDescription> getExportedObjects(EClass type, QualifiedName name, boolean ignoreCase) { | ||
48 | boolean startsWith = ignoreCase ? name.startsWithIgnoreCase(normalizedPrefix) : | ||
49 | name.startsWith(normalizedPrefix); | ||
50 | if (startsWith && name.getSegmentCount() > normalizedPrefix.getSegmentCount()) { | ||
51 | var originalName = originalPrefix.append(name.skipFirst(normalizedPrefix.getSegmentCount())); | ||
52 | return Iterables.transform( | ||
53 | delegateSelectable.getExportedObjects(type, originalName, ignoreCase), | ||
54 | description -> { | ||
55 | var normalizedName = normalizedPrefix.append( | ||
56 | description.getName().skipFirst(originalPrefix.getSegmentCount())); | ||
57 | return new AliasedEObjectDescription(normalizedName, description); | ||
58 | }); | ||
59 | } | ||
60 | return List.of(); | ||
61 | } | ||
62 | |||
63 | @Override | ||
64 | public Iterable<IEObjectDescription> getExportedObjectsByType(EClass type) { | ||
65 | var delegateIterable = delegateSelectable.getExportedObjectsByType(type); | ||
66 | return getAliasedElements(delegateIterable); | ||
67 | } | ||
68 | |||
69 | @Override | ||
70 | public Iterable<IEObjectDescription> getExportedObjectsByObject(EObject object) { | ||
71 | var delegateIterable = delegateSelectable.getExportedObjectsByObject(object); | ||
72 | return getAliasedElements(delegateIterable); | ||
73 | } | ||
74 | |||
75 | private Iterable<IEObjectDescription> getAliasedElements(Iterable<IEObjectDescription> delegateIterable) { | ||
76 | return () -> new Iterator<>() { | ||
77 | private final Iterator<IEObjectDescription> delegateIterator = delegateIterable.iterator(); | ||
78 | private IEObjectDescription next = computeNext(); | ||
79 | |||
80 | @Override | ||
81 | public boolean hasNext() { | ||
82 | return next != null; | ||
83 | } | ||
84 | |||
85 | @Override | ||
86 | public IEObjectDescription next() { | ||
87 | if (!hasNext()) { | ||
88 | throw new NoSuchElementException(); | ||
89 | } | ||
90 | var current = next; | ||
91 | next = computeNext(); | ||
92 | return current; | ||
93 | } | ||
94 | |||
95 | private IEObjectDescription computeNext() { | ||
96 | while (delegateIterator.hasNext()) { | ||
97 | var description = delegateIterator.next(); | ||
98 | var qualifiedName = description.getName(); | ||
99 | if (qualifiedName.startsWith(originalPrefix) && | ||
100 | qualifiedName.getSegmentCount() > originalPrefix.getSegmentCount()) { | ||
101 | var alias = normalizedPrefix.append(qualifiedName.skipFirst(originalPrefix.getSegmentCount())); | ||
102 | return new AliasedEObjectDescription(alias, description); | ||
103 | } | ||
104 | } | ||
105 | return null; | ||
106 | } | ||
107 | }; | ||
108 | } | ||
109 | } | ||
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 4d2dd772..65e80a7e 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 | |||
@@ -5,19 +5,85 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.scoping; | 6 | package tools.refinery.language.scoping; |
7 | 7 | ||
8 | import java.util.LinkedHashSet; | 8 | import com.google.common.base.Predicate; |
9 | 9 | import com.google.inject.Inject; | |
10 | import org.eclipse.emf.common.util.URI; | 10 | import com.google.inject.Provider; |
11 | import org.eclipse.emf.ecore.EClass; | ||
11 | import org.eclipse.emf.ecore.resource.Resource; | 12 | import org.eclipse.emf.ecore.resource.Resource; |
12 | import org.eclipse.xtext.scoping.impl.ImportUriGlobalScopeProvider; | 13 | import org.eclipse.xtext.naming.QualifiedName; |
14 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
15 | import org.eclipse.xtext.resource.ISelectable; | ||
16 | import org.eclipse.xtext.scoping.IScope; | ||
17 | import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeProvider; | ||
18 | import org.eclipse.xtext.util.IResourceScopeCache; | ||
19 | import tools.refinery.language.resource.LoadOnDemandResourceDescriptionProvider; | ||
20 | import tools.refinery.language.scoping.imports.ImportCollector; | ||
21 | import tools.refinery.language.scoping.imports.NamedImport; | ||
22 | |||
23 | import java.util.ArrayList; | ||
24 | import java.util.Collection; | ||
25 | import java.util.List; | ||
26 | |||
27 | public class ProblemGlobalScopeProvider extends AbstractGlobalScopeProvider { | ||
28 | private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemGlobalScopeProvider.CACHE_KEY"; | ||
13 | 29 | ||
14 | import tools.refinery.language.utils.ProblemUtil; | 30 | @Inject |
31 | private ImportCollector importCollector; | ||
15 | 32 | ||
16 | public class ProblemGlobalScopeProvider extends ImportUriGlobalScopeProvider { | 33 | @Inject |
34 | private Provider<LoadOnDemandResourceDescriptionProvider> loadOnDemandProvider; | ||
35 | |||
36 | @Inject | ||
37 | private IResourceScopeCache cache; | ||
38 | |||
39 | // {@link com.google.common.base.Predicate} required by Xtext API. | ||
40 | @SuppressWarnings("squid:S4738") | ||
17 | @Override | 41 | @Override |
18 | protected LinkedHashSet<URI> getImportedUris(Resource resource) { | 42 | protected IScope getScope(Resource resource, boolean ignoreCase, EClass type, |
19 | LinkedHashSet<URI> importedUris = new LinkedHashSet<>(); | 43 | Predicate<IEObjectDescription> filter) { |
20 | importedUris.add(ProblemUtil.BUILTIN_LIBRARY_URI); | 44 | var loadedImports = cache.get(CACHE_KEY, resource, () -> computeLoadedImports(resource)); |
21 | return importedUris; | 45 | var qualifiedScope = createScope(IScope.NULLSCOPE, loadedImports.qualifiedImports(), type, filter, ignoreCase); |
46 | var implicitScope = createScope(qualifiedScope, loadedImports.implicitImports(), type, filter, ignoreCase); | ||
47 | return createScope(implicitScope, loadedImports.explicitImports(), type, filter, ignoreCase); | ||
48 | } | ||
49 | |||
50 | protected LoadedImports computeLoadedImports(Resource resource) { | ||
51 | var imports = importCollector.getAllImports(resource); | ||
52 | var loadOnDemand = loadOnDemandProvider.get(); | ||
53 | loadOnDemand.setContext(resource); | ||
54 | var qualifiedImports = new ArrayList<ISelectable>(); | ||
55 | var implicitImports = new ArrayList<ISelectable>(); | ||
56 | var explicitImports = new ArrayList<ISelectable>(); | ||
57 | for (var importEntry : imports.toList()) { | ||
58 | var uri = importEntry.uri(); | ||
59 | var resourceDescription = loadOnDemand.getResourceDescription(uri); | ||
60 | if (resourceDescription == null) { | ||
61 | continue; | ||
62 | } | ||
63 | qualifiedImports.add(resourceDescription); | ||
64 | if (importEntry instanceof NamedImport namedImport) { | ||
65 | var qualifiedName = namedImport.qualifiedName(); | ||
66 | if (namedImport.alsoImplicit()) { | ||
67 | implicitImports.add(new NormalizedSelectable(resourceDescription, qualifiedName, | ||
68 | QualifiedName.EMPTY)); | ||
69 | } | ||
70 | for (var alias : namedImport.aliases()) { | ||
71 | explicitImports.add(new NormalizedSelectable(resourceDescription, qualifiedName, alias)); | ||
72 | } | ||
73 | } | ||
74 | } | ||
75 | return new LoadedImports(qualifiedImports, implicitImports, explicitImports); | ||
76 | } | ||
77 | |||
78 | // {@link com.google.common.base.Predicate} required by Xtext API. | ||
79 | @SuppressWarnings("squid:S4738") | ||
80 | protected IScope createScope(IScope parent, Collection<? extends ISelectable> children, EClass type, | ||
81 | Predicate<IEObjectDescription> filter, boolean ignoreCase) { | ||
82 | var selectable = CompositeSelectable.of(children); | ||
83 | return ShadowingKeyAwareSelectableBasedScope.createScope(parent, selectable, filter, type, ignoreCase); | ||
84 | } | ||
85 | |||
86 | protected record LoadedImports(List<ISelectable> qualifiedImports, List<ISelectable> implicitImports, | ||
87 | List<ISelectable> explicitImports) { | ||
22 | } | 88 | } |
23 | } | 89 | } |
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 229960a0..0067bf94 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 | |||
@@ -1,47 +1,68 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | package tools.refinery.language.scoping; | 6 | package tools.refinery.language.scoping; |
7 | 7 | ||
8 | import java.util.List; | 8 | import com.google.inject.Inject; |
9 | 9 | import com.google.inject.name.Named; | |
10 | import org.eclipse.emf.ecore.EObject; | 10 | import org.eclipse.emf.ecore.EObject; |
11 | import org.eclipse.emf.ecore.EReference; | ||
11 | import org.eclipse.emf.ecore.resource.Resource; | 12 | import org.eclipse.emf.ecore.resource.Resource; |
13 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | ||
12 | import org.eclipse.xtext.naming.QualifiedName; | 14 | import org.eclipse.xtext.naming.QualifiedName; |
13 | import org.eclipse.xtext.resource.IResourceDescriptions; | ||
14 | import org.eclipse.xtext.resource.IResourceDescriptionsProvider; | 15 | import org.eclipse.xtext.resource.IResourceDescriptionsProvider; |
15 | import org.eclipse.xtext.resource.ISelectable; | 16 | import org.eclipse.xtext.resource.ISelectable; |
16 | import org.eclipse.xtext.scoping.impl.ImportNormalizer; | 17 | import org.eclipse.xtext.scoping.IScope; |
17 | import org.eclipse.xtext.scoping.impl.ImportedNamespaceAwareLocalScopeProvider; | 18 | import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeDelegatingScopeProvider; |
19 | import org.eclipse.xtext.util.IResourceScopeCache; | ||
20 | import tools.refinery.language.naming.ProblemQualifiedNameProvider; | ||
18 | 21 | ||
19 | import com.google.inject.Inject; | 22 | public class ProblemLocalScopeProvider extends AbstractGlobalScopeDelegatingScopeProvider { |
23 | private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemLocalScopeProvider.CACHE_KEY"; | ||
20 | 24 | ||
21 | import tools.refinery.language.utils.ProblemUtil; | 25 | @Inject |
22 | 26 | @Named(ProblemQualifiedNameProvider.NAMED_DELEGATE) | |
23 | public class ProblemLocalScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { | 27 | private IQualifiedNameProvider delegateQualifiedNameProvider; |
24 | private static final QualifiedName BUILTIN_LIBRARY_QUALIFIED_NAME = QualifiedName | ||
25 | .create(ProblemUtil.BUILTIN_LIBRARY_NAME); | ||
26 | 28 | ||
27 | @Inject | 29 | @Inject |
28 | private IResourceDescriptionsProvider resourceDescriptionsProvider; | 30 | private IResourceDescriptionsProvider resourceDescriptionsProvider; |
29 | 31 | ||
30 | @Override | 32 | @Inject |
31 | protected List<ImportNormalizer> getImplicitImports(boolean ignoreCase) { | 33 | private IResourceScopeCache cache; |
32 | return List.of(doCreateImportNormalizer(BUILTIN_LIBRARY_QUALIFIED_NAME, true, ignoreCase)); | ||
33 | } | ||
34 | 34 | ||
35 | @Override | 35 | @Override |
36 | protected List<ImportNormalizer> getImportedNamespaceResolvers(EObject context, boolean ignoreCase) { | 36 | public IScope getScope(EObject context, EReference reference) { |
37 | return List.of(); | 37 | var resource = context.eResource(); |
38 | if (resource == null) { | ||
39 | return IScope.NULLSCOPE; | ||
40 | } | ||
41 | var globalScope = getGlobalScope(resource, reference); | ||
42 | var localImports = cache.get(CACHE_KEY, resource, () -> computeLocalImports(resource)); | ||
43 | if (localImports == null) { | ||
44 | return globalScope; | ||
45 | } | ||
46 | var type = reference.getEReferenceType(); | ||
47 | boolean ignoreCase = isIgnoreCase(reference); | ||
48 | return ShadowingKeyAwareSelectableBasedScope.createScope(globalScope, localImports, type, ignoreCase); | ||
38 | } | 49 | } |
39 | 50 | ||
40 | @Override | 51 | protected ISelectable computeLocalImports(Resource resource) { |
41 | protected ISelectable internalGetAllDescriptions(Resource resource) { | ||
42 | // Force the use of ProblemResourceDescriptionStrategy to include all QualifiedNames of objects. | 52 | // Force the use of ProblemResourceDescriptionStrategy to include all QualifiedNames of objects. |
43 | IResourceDescriptions resourceDescriptions = resourceDescriptionsProvider | 53 | var resourceDescriptions = resourceDescriptionsProvider.getResourceDescriptions(resource.getResourceSet()); |
44 | .getResourceDescriptions(resource.getResourceSet()); | 54 | var resourceDescription = resourceDescriptions.getResourceDescription(resource.getURI()); |
45 | return resourceDescriptions.getResourceDescription(resource.getURI()); | 55 | if (resourceDescription == null) { |
56 | return null; | ||
57 | } | ||
58 | var rootElement = resource.getContents().getFirst(); | ||
59 | if (rootElement == null) { | ||
60 | return new NoFullyQualifiedNamesSelectable(resourceDescription); | ||
61 | } | ||
62 | var rootName = delegateQualifiedNameProvider.getFullyQualifiedName(rootElement); | ||
63 | if (rootName == null) { | ||
64 | return new NoFullyQualifiedNamesSelectable(resourceDescription); | ||
65 | } | ||
66 | return new NormalizedSelectable(resourceDescription, rootName, QualifiedName.EMPTY); | ||
46 | } | 67 | } |
47 | } | 68 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/ShadowingKeyAwareSelectableBasedScope.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/ShadowingKeyAwareSelectableBasedScope.java new file mode 100644 index 00000000..cdef13ad --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/ShadowingKeyAwareSelectableBasedScope.java | |||
@@ -0,0 +1,52 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping; | ||
7 | |||
8 | import com.google.common.base.Predicate; | ||
9 | import org.eclipse.emf.ecore.EClass; | ||
10 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
11 | import org.eclipse.xtext.resource.ISelectable; | ||
12 | import org.eclipse.xtext.scoping.IScope; | ||
13 | import org.eclipse.xtext.scoping.impl.SelectableBasedScope; | ||
14 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | ||
15 | |||
16 | import java.util.Objects; | ||
17 | |||
18 | public class ShadowingKeyAwareSelectableBasedScope extends SelectableBasedScope { | ||
19 | public static IScope createScope(IScope outer, ISelectable selectable, EClass type, boolean ignoreCase) { | ||
20 | return createScope(outer, selectable, null, type, ignoreCase); | ||
21 | } | ||
22 | |||
23 | // {@link com.google.common.base.Predicate} required by Xtext API. | ||
24 | @SuppressWarnings("squid:S4738") | ||
25 | public static IScope createScope(IScope outer, ISelectable selectable, Predicate<IEObjectDescription> filter, | ||
26 | EClass type, boolean ignoreCase) { | ||
27 | if (selectable == null || selectable.isEmpty()) | ||
28 | return outer; | ||
29 | return new ShadowingKeyAwareSelectableBasedScope(outer, selectable, filter, type, ignoreCase); | ||
30 | } | ||
31 | |||
32 | // {@link com.google.common.base.Predicate} required by Xtext API. | ||
33 | @SuppressWarnings("squid:S4738") | ||
34 | protected ShadowingKeyAwareSelectableBasedScope(IScope outer, ISelectable selectable, | ||
35 | Predicate<IEObjectDescription> filter, | ||
36 | EClass type, boolean ignoreCase) { | ||
37 | super(outer, selectable, filter, type, ignoreCase); | ||
38 | } | ||
39 | |||
40 | @Override | ||
41 | protected boolean isShadowed(IEObjectDescription input) { | ||
42 | var shadowingKey = input.getUserData(ProblemResourceDescriptionStrategy.SHADOWING_KEY); | ||
43 | var localElements = getLocalElementsByName(input.getName()); | ||
44 | for (var localElement : localElements) { | ||
45 | var localElementKey = localElement.getUserData(ProblemResourceDescriptionStrategy.SHADOWING_KEY); | ||
46 | if (Objects.equals(shadowingKey, localElementKey)) { | ||
47 | return true; | ||
48 | } | ||
49 | } | ||
50 | return false; | ||
51 | } | ||
52 | } | ||
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping.imports; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | |||
10 | public sealed interface Import permits NamedImport, TransitiveImport { | ||
11 | URI uri(); | ||
12 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java new file mode 100644 index 00000000..d7a5304f --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java | |||
@@ -0,0 +1,205 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping.imports; | ||
7 | |||
8 | import com.google.common.base.Splitter; | ||
9 | import com.google.common.cache.Cache; | ||
10 | import com.google.common.cache.CacheBuilder; | ||
11 | import org.apache.log4j.Logger; | ||
12 | import org.eclipse.emf.common.notify.Notification; | ||
13 | import org.eclipse.emf.common.notify.impl.AdapterImpl; | ||
14 | import org.eclipse.emf.common.util.URI; | ||
15 | import org.eclipse.emf.ecore.resource.Resource; | ||
16 | import org.eclipse.emf.ecore.resource.ResourceSet; | ||
17 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
18 | import org.eclipse.xtext.naming.QualifiedName; | ||
19 | import tools.refinery.language.library.RefineryLibrary; | ||
20 | |||
21 | import java.io.File; | ||
22 | import java.nio.file.Path; | ||
23 | import java.util.*; | ||
24 | |||
25 | public class ImportAdapter extends AdapterImpl { | ||
26 | private static final Logger LOG = Logger.getLogger(ImportAdapter.class); | ||
27 | private static final List<RefineryLibrary> DEFAULT_LIBRARIES; | ||
28 | private static final List<Path> DEFAULT_PATHS; | ||
29 | |||
30 | static { | ||
31 | var serviceLoader = ServiceLoader.load(RefineryLibrary.class); | ||
32 | var defaultLibraries = new ArrayList<RefineryLibrary>(); | ||
33 | for (var service : serviceLoader) { | ||
34 | defaultLibraries.add(service); | ||
35 | } | ||
36 | DEFAULT_LIBRARIES = List.copyOf(defaultLibraries); | ||
37 | var pathEnv = System.getenv("REFINERY_LIBRARY_PATH"); | ||
38 | if (pathEnv == null) { | ||
39 | DEFAULT_PATHS = List.of(); | ||
40 | } else { | ||
41 | DEFAULT_PATHS = Splitter.on(File.pathSeparatorChar) | ||
42 | .splitToStream(pathEnv) | ||
43 | .map(pathString -> Path.of(pathString).toAbsolutePath().normalize()) | ||
44 | .toList(); | ||
45 | } | ||
46 | } | ||
47 | |||
48 | private final List<RefineryLibrary> libraries; | ||
49 | private final List<Path> libraryPaths; | ||
50 | private final Cache<QualifiedName, QualifiedName> failedResolutions = | ||
51 | CacheBuilder.newBuilder().maximumSize(100).build(); | ||
52 | private final Map<QualifiedName, URI> qualifiedNameToUriMap = new LinkedHashMap<>(); | ||
53 | private final Map<URI, QualifiedName> uriToQualifiedNameMap = new LinkedHashMap<>(); | ||
54 | |||
55 | private ImportAdapter(ResourceSet resourceSet) { | ||
56 | libraries = new ArrayList<>(DEFAULT_LIBRARIES); | ||
57 | libraryPaths = new ArrayList<>(DEFAULT_PATHS); | ||
58 | for (var resource : resourceSet.getResources()) { | ||
59 | resourceAdded(resource); | ||
60 | } | ||
61 | } | ||
62 | |||
63 | @Override | ||
64 | public boolean isAdapterForType(Object type) { | ||
65 | return type == ImportAdapter.class; | ||
66 | } | ||
67 | |||
68 | public List<RefineryLibrary> getLibraries() { | ||
69 | return libraries; | ||
70 | } | ||
71 | |||
72 | public List<Path> getLibraryPaths() { | ||
73 | return libraryPaths; | ||
74 | } | ||
75 | |||
76 | public URI resolveQualifiedName(QualifiedName qualifiedName) { | ||
77 | var uri = getResolvedUri(qualifiedName); | ||
78 | if (uri != null) { | ||
79 | return uri; | ||
80 | } | ||
81 | if (isFailed(qualifiedName)) { | ||
82 | return null; | ||
83 | } | ||
84 | for (var library : libraries) { | ||
85 | var result = library.resolveQualifiedName(qualifiedName, libraryPaths); | ||
86 | if (result.isPresent()) { | ||
87 | uri = result.get(); | ||
88 | markAsResolved(qualifiedName, uri); | ||
89 | return uri; | ||
90 | } | ||
91 | } | ||
92 | markAsUnresolved(qualifiedName); | ||
93 | return null; | ||
94 | } | ||
95 | |||
96 | private URI getResolvedUri(QualifiedName qualifiedName) { | ||
97 | return qualifiedNameToUriMap.get(qualifiedName); | ||
98 | } | ||
99 | |||
100 | private boolean isFailed(QualifiedName qualifiedName) { | ||
101 | return failedResolutions.getIfPresent(qualifiedName) != null; | ||
102 | } | ||
103 | |||
104 | private void markAsResolved(QualifiedName qualifiedName, URI uri) { | ||
105 | if (qualifiedNameToUriMap.put(qualifiedName, uri) != null) { | ||
106 | throw new IllegalArgumentException("Already resolved " + qualifiedName); | ||
107 | } | ||
108 | // We don't need to signal an error here, because modules with multiple qualified names will lead to | ||
109 | // validation errors later. | ||
110 | uriToQualifiedNameMap.putIfAbsent(uri, qualifiedName); | ||
111 | failedResolutions.invalidate(qualifiedName); | ||
112 | } | ||
113 | |||
114 | private void markAsUnresolved(QualifiedName qualifiedName) { | ||
115 | failedResolutions.put(qualifiedName, qualifiedName); | ||
116 | } | ||
117 | |||
118 | public QualifiedName getQualifiedName(URI uri) { | ||
119 | return uriToQualifiedNameMap.get(uri); | ||
120 | } | ||
121 | |||
122 | @Override | ||
123 | public void notifyChanged(Notification msg) { | ||
124 | switch (msg.getEventType()) { | ||
125 | case Notification.ADD -> { | ||
126 | if (msg.getNewValue() instanceof Resource resource) { | ||
127 | resourceAdded(resource); | ||
128 | } | ||
129 | } | ||
130 | case Notification.ADD_MANY -> { | ||
131 | if (msg.getNewValue() instanceof List<?> list) { | ||
132 | manyResourcesAdded(list); | ||
133 | } | ||
134 | } | ||
135 | case Notification.REMOVE -> { | ||
136 | if (msg.getOldValue() instanceof Resource resource) { | ||
137 | resourceRemoved(resource); | ||
138 | } | ||
139 | } | ||
140 | case Notification.REMOVE_MANY -> { | ||
141 | if (msg.getOldValue() instanceof List<?> list) { | ||
142 | manyResourcesRemoved(list); | ||
143 | } | ||
144 | } | ||
145 | default -> { | ||
146 | // Nothing to update. | ||
147 | } | ||
148 | } | ||
149 | } | ||
150 | |||
151 | private void manyResourcesAdded(List<?> list) { | ||
152 | for (var element : list) { | ||
153 | if (element instanceof Resource resource) { | ||
154 | resourceAdded(resource); | ||
155 | } | ||
156 | } | ||
157 | } | ||
158 | |||
159 | private void manyResourcesRemoved(List<?> list) { | ||
160 | for (var element : list) { | ||
161 | if (element instanceof Resource resource) { | ||
162 | resourceRemoved(resource); | ||
163 | } | ||
164 | } | ||
165 | } | ||
166 | |||
167 | private void resourceAdded(Resource resource) { | ||
168 | var uri = resource.getURI(); | ||
169 | for (var library : libraries) { | ||
170 | var result = library.getQualifiedName(uri, libraryPaths); | ||
171 | if (result.isPresent()) { | ||
172 | var qualifiedName = result.get(); | ||
173 | var previousQualifiedName = uriToQualifiedNameMap.putIfAbsent(uri, qualifiedName); | ||
174 | if (previousQualifiedName == null) { | ||
175 | if (qualifiedNameToUriMap.put(qualifiedName, uri) != null) { | ||
176 | throw new IllegalArgumentException("Duplicate resource for" + qualifiedName); | ||
177 | } | ||
178 | } else if (!previousQualifiedName.equals(qualifiedName)) { | ||
179 | LOG.warn("Expected %s to have qualified name %s, got %s instead".formatted( | ||
180 | uri, previousQualifiedName, qualifiedName)); | ||
181 | } | ||
182 | } | ||
183 | } | ||
184 | } | ||
185 | |||
186 | private void resourceRemoved(Resource resource) { | ||
187 | var qualifiedName = uriToQualifiedNameMap.remove(resource.getURI()); | ||
188 | if (qualifiedName != null) { | ||
189 | qualifiedNameToUriMap.remove(qualifiedName); | ||
190 | } | ||
191 | } | ||
192 | |||
193 | public static ImportAdapter getOrInstall(ResourceSet resourceSet) { | ||
194 | var adapter = getAdapter(resourceSet); | ||
195 | if (adapter == null) { | ||
196 | adapter = new ImportAdapter(resourceSet); | ||
197 | resourceSet.eAdapters().add(adapter); | ||
198 | } | ||
199 | return adapter; | ||
200 | } | ||
201 | |||
202 | private static ImportAdapter getAdapter(ResourceSet resourceSet) { | ||
203 | return (ImportAdapter) EcoreUtil.getAdapter(resourceSet.eAdapters(), ImportAdapter.class); | ||
204 | } | ||
205 | } | ||
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping.imports; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.jetbrains.annotations.NotNull; | ||
10 | import org.jetbrains.annotations.Nullable; | ||
11 | |||
12 | import java.util.*; | ||
13 | |||
14 | public class ImportCollection { | ||
15 | public static ImportCollection EMPTY = new ImportCollection() { | ||
16 | @Override | ||
17 | public void add(Import importEntry) { | ||
18 | throw new UnsupportedOperationException("Read-only collection"); | ||
19 | } | ||
20 | |||
21 | @Override | ||
22 | public void remove(URI uri) { | ||
23 | throw new UnsupportedOperationException("Read-only collection"); | ||
24 | } | ||
25 | }; | ||
26 | |||
27 | private final Map<URI, Import> importMap = new HashMap<>(); | ||
28 | |||
29 | public void add(Import importEntry) { | ||
30 | importMap.compute(importEntry.uri(), (ignored, originalEntry) -> merge(originalEntry, importEntry)); | ||
31 | } | ||
32 | |||
33 | public void addAll(Iterable<? extends Import> imports) { | ||
34 | imports.forEach(this::add); | ||
35 | } | ||
36 | |||
37 | public void remove(URI uri) { | ||
38 | importMap.remove(uri); | ||
39 | } | ||
40 | |||
41 | public List<Import> toList() { | ||
42 | return List.copyOf(importMap.values()); | ||
43 | } | ||
44 | |||
45 | public Set<URI> toUriSet() { | ||
46 | return new LinkedHashSet<>(importMap.keySet()); | ||
47 | } | ||
48 | |||
49 | @NotNull | ||
50 | private static Import merge(@Nullable Import left, @NotNull Import right) { | ||
51 | if (left == null) { | ||
52 | return right; | ||
53 | } | ||
54 | if (!left.uri().equals(right.uri())) { | ||
55 | throw new IllegalArgumentException("Expected URIs '%s' and '%s' to be equal".formatted( | ||
56 | left.uri(), right.uri())); | ||
57 | } | ||
58 | return switch (left) { | ||
59 | case TransitiveImport transitiveLeft -> | ||
60 | right instanceof TransitiveImport ? left : merge(right, transitiveLeft); | ||
61 | case NamedImport namedLeft -> switch (right) { | ||
62 | case TransitiveImport ignored -> namedLeft; | ||
63 | case NamedImport namedRight -> { | ||
64 | if (!namedLeft.qualifiedName().equals(namedRight.qualifiedName())) { | ||
65 | throw new IllegalArgumentException("Expected qualified names '%s' and '%s' to be equal" | ||
66 | .formatted(namedLeft.qualifiedName(), namedRight.qualifiedName())); | ||
67 | } | ||
68 | var mergedAliases = new LinkedHashSet<>(namedLeft.aliases()); | ||
69 | mergedAliases.addAll(namedRight.aliases()); | ||
70 | yield new NamedImport(namedLeft.uri(), namedLeft.qualifiedName(), | ||
71 | List.copyOf(mergedAliases), namedLeft.alsoImplicit() || namedRight.alsoImplicit()); | ||
72 | } | ||
73 | }; | ||
74 | }; | ||
75 | } | ||
76 | } | ||
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..ac5a92ba --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java | |||
@@ -0,0 +1,160 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping.imports; | ||
7 | |||
8 | import com.google.common.base.Splitter; | ||
9 | import com.google.common.base.Strings; | ||
10 | import com.google.inject.Inject; | ||
11 | import com.google.inject.Provider; | ||
12 | import com.google.inject.Singleton; | ||
13 | import org.eclipse.emf.common.util.URI; | ||
14 | import org.eclipse.emf.ecore.EObject; | ||
15 | import org.eclipse.emf.ecore.resource.Resource; | ||
16 | import org.eclipse.xtext.linking.impl.LinkingHelper; | ||
17 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
18 | import org.eclipse.xtext.naming.QualifiedName; | ||
19 | import org.eclipse.xtext.nodemodel.util.NodeModelUtils; | ||
20 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
21 | import org.eclipse.xtext.util.IResourceScopeCache; | ||
22 | import tools.refinery.language.model.problem.ImportStatement; | ||
23 | import tools.refinery.language.model.problem.Problem; | ||
24 | import tools.refinery.language.model.problem.ProblemPackage; | ||
25 | import tools.refinery.language.naming.NamingUtil; | ||
26 | import tools.refinery.language.resource.LoadOnDemandResourceDescriptionProvider; | ||
27 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | ||
28 | |||
29 | import java.util.*; | ||
30 | |||
31 | @Singleton | ||
32 | public class ImportCollector { | ||
33 | private static final String PREFIX = "tools.refinery.language.imports."; | ||
34 | private static final String DIRECT_IMPORTS_KEY = PREFIX + "DIRECT_IMPORTS"; | ||
35 | private static final String ALL_IMPORTS_KEY = PREFIX + "ALL_IMPORTS"; | ||
36 | |||
37 | @Inject | ||
38 | private IResourceScopeCache cache; | ||
39 | |||
40 | @Inject | ||
41 | private LinkingHelper linkingHelper; | ||
42 | |||
43 | @Inject | ||
44 | private IQualifiedNameConverter qualifiedNameConverter; | ||
45 | |||
46 | @Inject | ||
47 | private Provider<LoadOnDemandResourceDescriptionProvider> loadOnDemandProvider; | ||
48 | |||
49 | public ImportCollection getDirectImports(Resource resource) { | ||
50 | return cache.get(DIRECT_IMPORTS_KEY, resource, () -> this.computeDirectImports(resource)); | ||
51 | } | ||
52 | |||
53 | protected ImportCollection computeDirectImports(Resource resource) { | ||
54 | if (resource.getContents().isEmpty() || !(resource.getContents().getFirst() instanceof Problem problem)) { | ||
55 | return ImportCollection.EMPTY; | ||
56 | } | ||
57 | var resourceSet = resource.getResourceSet(); | ||
58 | if (resourceSet == null) { | ||
59 | return ImportCollection.EMPTY; | ||
60 | } | ||
61 | var adapter = ImportAdapter.getOrInstall(resourceSet); | ||
62 | var collection = new ImportCollection(); | ||
63 | collectAutomaticImports(collection, adapter); | ||
64 | collectExplicitImports(problem, collection, adapter); | ||
65 | collection.remove(resource.getURI()); | ||
66 | return collection; | ||
67 | } | ||
68 | |||
69 | private void collectAutomaticImports(ImportCollection importCollection, ImportAdapter adapter) { | ||
70 | for (var library : adapter.getLibraries()) { | ||
71 | for (var qualifiedName : library.getAutomaticImports()) { | ||
72 | var uri = adapter.resolveQualifiedName(qualifiedName); | ||
73 | if (uri != null) { | ||
74 | importCollection.add(NamedImport.implicit(uri, qualifiedName)); | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | |||
80 | private void collectExplicitImports(Problem problem, ImportCollection collection, ImportAdapter adapter) { | ||
81 | for (var statement : problem.getStatements()) { | ||
82 | if (statement instanceof ImportStatement importStatement) { | ||
83 | collectImportStatement(importStatement, collection, adapter); | ||
84 | } | ||
85 | } | ||
86 | } | ||
87 | |||
88 | private void collectImportStatement(ImportStatement importStatement, ImportCollection collection, | ||
89 | ImportAdapter adapter) { | ||
90 | var nodes = NodeModelUtils.findNodesForFeature(importStatement, | ||
91 | ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE); | ||
92 | var aliasString = importStatement.getAlias(); | ||
93 | var alias = Strings.isNullOrEmpty(aliasString) ? QualifiedName.EMPTY : | ||
94 | NamingUtil.stripRootPrefix(qualifiedNameConverter.toQualifiedName(aliasString)); | ||
95 | var referredProblem = (EObject) importStatement.eGet(ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE, | ||
96 | false); | ||
97 | URI referencedUri = null; | ||
98 | if (referredProblem != null && !referredProblem.eIsProxy()) { | ||
99 | var resource = referredProblem.eResource(); | ||
100 | if (resource != null) { | ||
101 | referencedUri = resource.getURI(); | ||
102 | } | ||
103 | } | ||
104 | for (var node : nodes) { | ||
105 | var qualifiedNameString = linkingHelper.getCrossRefNodeAsString(node, true); | ||
106 | if (Strings.isNullOrEmpty(qualifiedNameString)) { | ||
107 | continue; | ||
108 | } | ||
109 | var qualifiedName = NamingUtil.stripRootPrefix( | ||
110 | qualifiedNameConverter.toQualifiedName(qualifiedNameString)); | ||
111 | var uri = referencedUri == null ? adapter.resolveQualifiedName(qualifiedName) : referencedUri; | ||
112 | if (uri != null) { | ||
113 | collection.add(NamedImport.explicit(uri, qualifiedName, List.of(alias))); | ||
114 | } | ||
115 | } | ||
116 | } | ||
117 | |||
118 | public ImportCollection getAllImports(Resource resource) { | ||
119 | return cache.get(ALL_IMPORTS_KEY, resource, () -> this.computeAllImports(resource)); | ||
120 | } | ||
121 | |||
122 | protected ImportCollection computeAllImports(Resource resource) { | ||
123 | var collection = new ImportCollection(); | ||
124 | collection.addAll(getDirectImports(resource).toList()); | ||
125 | var loadOnDemand = loadOnDemandProvider.get(); | ||
126 | loadOnDemand.setContext(resource); | ||
127 | var seen = new HashSet<URI>(); | ||
128 | seen.add(resource.getURI()); | ||
129 | var queue = new ArrayDeque<>(collection.toUriSet()); | ||
130 | while (!queue.isEmpty()) { | ||
131 | var uri = queue.removeFirst(); | ||
132 | seen.add(uri); | ||
133 | collection.add(new TransitiveImport(uri)); | ||
134 | var resourceDescription = loadOnDemand.getResourceDescription(uri); | ||
135 | if (resourceDescription == null) { | ||
136 | continue; | ||
137 | } | ||
138 | var problemDescriptions = resourceDescription.getExportedObjectsByType(ProblemPackage.Literals.PROBLEM); | ||
139 | for (var eObjectDescription : problemDescriptions) { | ||
140 | for (var importedUri : getImports(eObjectDescription)) { | ||
141 | if (!seen.contains(importedUri)) { | ||
142 | queue.addLast(importedUri); | ||
143 | } | ||
144 | } | ||
145 | } | ||
146 | } | ||
147 | collection.remove(resource.getURI()); | ||
148 | return collection; | ||
149 | } | ||
150 | |||
151 | protected List<URI> getImports(IEObjectDescription eObjectDescription) { | ||
152 | var importString = eObjectDescription.getUserData(ProblemResourceDescriptionStrategy.IMPORTS); | ||
153 | if (importString == null || importString.isEmpty()) { | ||
154 | return List.of(); | ||
155 | } | ||
156 | return Splitter.on(ProblemResourceDescriptionStrategy.IMPORTS_SEPARATOR).splitToStream(importString) | ||
157 | .map(URI::createURI) | ||
158 | .toList(); | ||
159 | } | ||
160 | } | ||
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping.imports; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | ||
10 | |||
11 | import java.util.List; | ||
12 | |||
13 | public record NamedImport(URI uri, QualifiedName qualifiedName, List<QualifiedName> aliases, | ||
14 | boolean alsoImplicit) implements Import { | ||
15 | public static NamedImport implicit(URI uri, QualifiedName qualifiedName) { | ||
16 | return new NamedImport(uri, qualifiedName, List.of(), true); | ||
17 | } | ||
18 | |||
19 | public static NamedImport explicit(URI uri, QualifiedName qualifiedName, List<QualifiedName> aliases) { | ||
20 | return new NamedImport(uri, qualifiedName, aliases, false); | ||
21 | } | ||
22 | } | ||
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping.imports; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | |||
10 | public record TransitiveImport(URI uri) implements Import { | ||
11 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/serializer/ProblemTransientValueService.java b/subprojects/language/src/main/java/tools/refinery/language/serializer/ProblemTransientValueService.java new file mode 100644 index 00000000..269e1243 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/serializer/ProblemTransientValueService.java | |||
@@ -0,0 +1,24 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.serializer; | ||
7 | |||
8 | import org.eclipse.emf.ecore.EObject; | ||
9 | import org.eclipse.emf.ecore.EStructuralFeature; | ||
10 | import org.eclipse.xtext.parsetree.reconstr.impl.DefaultTransientValueService; | ||
11 | import tools.refinery.language.model.problem.Problem; | ||
12 | import tools.refinery.language.model.problem.ProblemPackage; | ||
13 | import tools.refinery.language.utils.ProblemUtil; | ||
14 | |||
15 | public class ProblemTransientValueService extends DefaultTransientValueService { | ||
16 | @Override | ||
17 | public boolean isTransient(EObject owner, EStructuralFeature feature, int index) { | ||
18 | if (owner instanceof Problem problem && feature == ProblemPackage.Literals.PROBLEM__KIND) { | ||
19 | return problem.getName() == null && problem.getKind() == ProblemUtil.getDefaultModuleKind(problem) && | ||
20 | !problem.isExplicitKind(); | ||
21 | } | ||
22 | return super.isTransient(owner, feature, index); | ||
23 | } | ||
24 | } | ||
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 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -11,6 +11,7 @@ import org.eclipse.emf.ecore.EObject; | |||
11 | import org.eclipse.emf.ecore.resource.Resource; | 11 | import org.eclipse.emf.ecore.resource.Resource; |
12 | import org.eclipse.xtext.util.IResourceScopeCache; | 12 | import org.eclipse.xtext.util.IResourceScopeCache; |
13 | import org.eclipse.xtext.util.Tuples; | 13 | import org.eclipse.xtext.util.Tuples; |
14 | import tools.refinery.language.library.BuiltinLibrary; | ||
14 | import tools.refinery.language.model.problem.*; | 15 | import tools.refinery.language.model.problem.*; |
15 | 16 | ||
16 | import java.util.*; | 17 | import java.util.*; |
@@ -27,8 +28,8 @@ public class ProblemDesugarer { | |||
27 | 28 | ||
28 | private Optional<Problem> doGetBuiltinProblem(Resource resource) { | 29 | private Optional<Problem> doGetBuiltinProblem(Resource resource) { |
29 | return Optional.ofNullable(resource).map(Resource::getResourceSet) | 30 | return Optional.ofNullable(resource).map(Resource::getResourceSet) |
30 | .map(resourceSet -> resourceSet.getResource(ProblemUtil.BUILTIN_LIBRARY_URI, true)) | 31 | .map(resourceSet -> resourceSet.getResource(BuiltinLibrary.BUILTIN_LIBRARY_URI, true)) |
31 | .map(Resource::getContents).filter(contents -> !contents.isEmpty()).map(contents -> contents.get(0)) | 32 | .map(Resource::getContents).filter(contents -> !contents.isEmpty()).map(List::getFirst) |
32 | .filter(Problem.class::isInstance).map(Problem.class::cast); | 33 | .filter(Problem.class::isInstance).map(Problem.class::cast); |
33 | } | 34 | } |
34 | 35 | ||
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 7b6407e1..f70893e0 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java | |||
@@ -8,11 +8,12 @@ package tools.refinery.language.utils; | |||
8 | import org.eclipse.emf.common.util.URI; | 8 | import org.eclipse.emf.common.util.URI; |
9 | import org.eclipse.emf.ecore.EObject; | 9 | import org.eclipse.emf.ecore.EObject; |
10 | import org.eclipse.emf.ecore.util.EcoreUtil; | 10 | import org.eclipse.emf.ecore.util.EcoreUtil; |
11 | import org.eclipse.xtext.EcoreUtil2; | ||
12 | import tools.refinery.language.library.BuiltinLibrary; | ||
11 | import tools.refinery.language.model.problem.*; | 13 | import tools.refinery.language.model.problem.*; |
12 | 14 | ||
13 | public final class ProblemUtil { | 15 | public final class ProblemUtil { |
14 | public static final String BUILTIN_LIBRARY_NAME = "builtin"; | 16 | public static final String MODULE_EXTENSION = "refinery"; |
15 | public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(); | ||
16 | 17 | ||
17 | private ProblemUtil() { | 18 | private ProblemUtil() { |
18 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); | 19 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); |
@@ -22,7 +23,7 @@ public final class ProblemUtil { | |||
22 | if (eObject != null) { | 23 | if (eObject != null) { |
23 | var eResource = eObject.eResource(); | 24 | var eResource = eObject.eResource(); |
24 | if (eResource != null) { | 25 | if (eResource != null) { |
25 | return ProblemUtil.BUILTIN_LIBRARY_URI.equals(eResource.getURI()); | 26 | return BuiltinLibrary.BUILTIN_LIBRARY_URI.equals(eResource.getURI()); |
26 | } | 27 | } |
27 | } | 28 | } |
28 | return false; | 29 | return false; |
@@ -54,14 +55,24 @@ public final class ProblemUtil { | |||
54 | return eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError(); | 55 | return eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError(); |
55 | } | 56 | } |
56 | 57 | ||
57 | public static boolean isIndividualNode(Node node) { | 58 | public static boolean isAtomNode(Node node) { |
58 | var containingFeature = node.eContainingFeature(); | 59 | var containingFeature = node.eContainingFeature(); |
59 | return containingFeature == ProblemPackage.Literals.INDIVIDUAL_DECLARATION__NODES | 60 | if (containingFeature == ProblemPackage.Literals.NODE_DECLARATION__NODES) { |
60 | || containingFeature == ProblemPackage.Literals.ENUM_DECLARATION__LITERALS; | 61 | return ((NodeDeclaration) node.eContainer()).getKind() == NodeKind.ATOM; |
62 | } | ||
63 | return containingFeature == ProblemPackage.Literals.ENUM_DECLARATION__LITERALS; | ||
64 | } | ||
65 | |||
66 | public static boolean isMultiNode(Node node) { | ||
67 | var containingFeature = node.eContainingFeature(); | ||
68 | if (containingFeature == ProblemPackage.Literals.NODE_DECLARATION__NODES) { | ||
69 | return ((NodeDeclaration) node.eContainer()).getKind() == NodeKind.MULTI; | ||
70 | } | ||
71 | return containingFeature == ProblemPackage.Literals.CLASS_DECLARATION__NEW_NODE; | ||
61 | } | 72 | } |
62 | 73 | ||
63 | public static boolean isNewNode(Node node) { | 74 | public static boolean isDeclaredNode(Node node) { |
64 | return node.eContainingFeature() == ProblemPackage.Literals.CLASS_DECLARATION__NEW_NODE; | 75 | return node.eContainingFeature() == ProblemPackage.Literals.NODE_DECLARATION__NODES; |
65 | } | 76 | } |
66 | 77 | ||
67 | public static boolean isInvalidMultiplicityConstraint(PredicateDefinition predicateDefinition) { | 78 | public static boolean isInvalidMultiplicityConstraint(PredicateDefinition predicateDefinition) { |
@@ -116,12 +127,24 @@ public final class ProblemUtil { | |||
116 | }; | 127 | }; |
117 | } | 128 | } |
118 | 129 | ||
119 | private static URI getLibraryUri() { | 130 | public static ModuleKind getDefaultModuleKind(Problem problem) { |
120 | var libraryResource = ProblemUtil.class.getClassLoader() | 131 | var resource = problem.eResource(); |
121 | .getResource("tools/refinery/language/%s.problem".formatted(BUILTIN_LIBRARY_NAME)); | 132 | if (resource == null) { |
122 | if (libraryResource == null) { | 133 | return ModuleKind.PROBLEM; |
123 | throw new AssertionError("Library '%s' was not found".formatted(BUILTIN_LIBRARY_NAME)); | ||
124 | } | 134 | } |
125 | return URI.createURI(libraryResource.toString()); | 135 | return getDefaultModuleKind(resource.getURI()); |
136 | } | ||
137 | |||
138 | public static ModuleKind getDefaultModuleKind(URI uri) { | ||
139 | return MODULE_EXTENSION.equals(uri.fileExtension()) ? ModuleKind.MODULE : ModuleKind.PROBLEM; | ||
140 | } | ||
141 | |||
142 | public static boolean isModule(Problem problem) { | ||
143 | return problem.getKind() == ModuleKind.MODULE; | ||
144 | } | ||
145 | |||
146 | public static boolean isInModule(EObject eObject) { | ||
147 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); | ||
148 | return problem != null && isModule(problem); | ||
126 | } | 149 | } |
127 | } | 150 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java index 8bda4b95..d9eb5fd3 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java +++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -13,16 +13,16 @@ import com.google.inject.Inject; | |||
13 | import org.eclipse.emf.ecore.EObject; | 13 | import org.eclipse.emf.ecore.EObject; |
14 | import org.eclipse.emf.ecore.EReference; | 14 | import org.eclipse.emf.ecore.EReference; |
15 | import org.eclipse.xtext.EcoreUtil2; | 15 | import org.eclipse.xtext.EcoreUtil2; |
16 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
16 | import org.eclipse.xtext.validation.Check; | 17 | import org.eclipse.xtext.validation.Check; |
17 | import org.jetbrains.annotations.Nullable; | 18 | import org.jetbrains.annotations.Nullable; |
18 | import tools.refinery.language.model.problem.*; | 19 | import tools.refinery.language.model.problem.*; |
20 | import tools.refinery.language.naming.NamingUtil; | ||
21 | import tools.refinery.language.scoping.imports.ImportAdapter; | ||
19 | import tools.refinery.language.utils.ProblemDesugarer; | 22 | import tools.refinery.language.utils.ProblemDesugarer; |
20 | import tools.refinery.language.utils.ProblemUtil; | 23 | import tools.refinery.language.utils.ProblemUtil; |
21 | 24 | ||
22 | import java.util.ArrayList; | 25 | import java.util.*; |
23 | import java.util.LinkedHashMap; | ||
24 | import java.util.LinkedHashSet; | ||
25 | import java.util.Set; | ||
26 | 26 | ||
27 | /** | 27 | /** |
28 | * This class contains custom validation rules. | 28 | * This class contains custom validation rules. |
@@ -33,6 +33,10 @@ import java.util.Set; | |||
33 | public class ProblemValidator extends AbstractProblemValidator { | 33 | public class ProblemValidator extends AbstractProblemValidator { |
34 | private static final String ISSUE_PREFIX = "tools.refinery.language.validation.ProblemValidator."; | 34 | private static final String ISSUE_PREFIX = "tools.refinery.language.validation.ProblemValidator."; |
35 | 35 | ||
36 | public static final String UNEXPECTED_MODULE_NAME_ISSUE = ISSUE_PREFIX + "UNEXPECTED_MODULE_NAME"; | ||
37 | |||
38 | public static final String INVALID_IMPORT_ISSUE = ISSUE_PREFIX + "INVALID_IMPORT"; | ||
39 | |||
36 | public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; | 40 | public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; |
37 | 41 | ||
38 | public static final String NODE_CONSTANT_ISSUE = ISSUE_PREFIX + "NODE_CONSTANT_ISSUE"; | 42 | public static final String NODE_CONSTANT_ISSUE = ISSUE_PREFIX + "NODE_CONSTANT_ISSUE"; |
@@ -65,6 +69,61 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
65 | @Inject | 69 | @Inject |
66 | private ProblemDesugarer desugarer; | 70 | private ProblemDesugarer desugarer; |
67 | 71 | ||
72 | @Inject | ||
73 | private IQualifiedNameConverter qualifiedNameConverter; | ||
74 | |||
75 | @Check | ||
76 | public void checkModuleName(Problem problem) { | ||
77 | var nameString = problem.getName(); | ||
78 | if (nameString == null) { | ||
79 | return; | ||
80 | } | ||
81 | var resource = problem.eResource(); | ||
82 | if (resource == null) { | ||
83 | return; | ||
84 | } | ||
85 | var resourceSet = resource.getResourceSet(); | ||
86 | if (resourceSet == null) { | ||
87 | return; | ||
88 | } | ||
89 | var adapter = ImportAdapter.getOrInstall(resourceSet); | ||
90 | var expectedName = adapter.getQualifiedName(resource.getURI()); | ||
91 | if (expectedName == null) { | ||
92 | return; | ||
93 | } | ||
94 | var name = NamingUtil.stripRootPrefix(qualifiedNameConverter.toQualifiedName(nameString)); | ||
95 | if (!expectedName.equals(name)) { | ||
96 | var moduleKindName = switch (problem.getKind()) { | ||
97 | case PROBLEM -> "problem"; | ||
98 | case MODULE -> "module"; | ||
99 | }; | ||
100 | var message = "Expected %s to have name '%s', got '%s' instead.".formatted( | ||
101 | moduleKindName, qualifiedNameConverter.toString(expectedName), | ||
102 | qualifiedNameConverter.toString(name)); | ||
103 | error(message, problem, ProblemPackage.Literals.NAMED_ELEMENT__NAME, INSIGNIFICANT_INDEX, | ||
104 | UNEXPECTED_MODULE_NAME_ISSUE); | ||
105 | } | ||
106 | } | ||
107 | |||
108 | @Check | ||
109 | public void checkImportStatement(ImportStatement importStatement) { | ||
110 | var importedModule = importStatement.getImportedModule(); | ||
111 | if (importedModule == null || importedModule.eIsProxy()) { | ||
112 | return; | ||
113 | } | ||
114 | String message = null; | ||
115 | var problem = EcoreUtil2.getContainerOfType(importStatement, Problem.class); | ||
116 | if (importedModule == problem) { | ||
117 | message = "A module cannot import itself."; | ||
118 | } else if (importedModule.getKind() != ModuleKind.MODULE) { | ||
119 | message = "Only modules can be imported."; | ||
120 | } | ||
121 | if (message != null) { | ||
122 | error(message, importStatement, ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE, | ||
123 | INSIGNIFICANT_INDEX, INVALID_IMPORT_ISSUE); | ||
124 | } | ||
125 | } | ||
126 | |||
68 | @Check | 127 | @Check |
69 | public void checkSingletonVariable(VariableOrNodeExpr expr) { | 128 | public void checkSingletonVariable(VariableOrNodeExpr expr) { |
70 | var variableOrNode = expr.getVariableOrNode(); | 129 | var variableOrNode = expr.getVariableOrNode(); |
@@ -84,10 +143,10 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
84 | @Check | 143 | @Check |
85 | public void checkNodeConstants(VariableOrNodeExpr expr) { | 144 | public void checkNodeConstants(VariableOrNodeExpr expr) { |
86 | var variableOrNode = expr.getVariableOrNode(); | 145 | var variableOrNode = expr.getVariableOrNode(); |
87 | if (variableOrNode instanceof Node node && !ProblemUtil.isIndividualNode(node)) { | 146 | if (variableOrNode instanceof Node node && !ProblemUtil.isAtomNode(node)) { |
88 | var name = node.getName(); | 147 | var name = node.getName(); |
89 | var message = ("Only individuals can be referenced in predicates. " + | 148 | var message = ("Only atoms can be referenced in predicates. " + |
90 | "Mark '%s' as individual with the declaration 'indiv %s.'").formatted(name, name); | 149 | "Mark '%s' as an atom with the declaration 'atom %s.'").formatted(name, name); |
91 | error(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, | 150 | error(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, |
92 | INSIGNIFICANT_INDEX, NODE_CONSTANT_ISSUE); | 151 | INSIGNIFICANT_INDEX, NODE_CONSTANT_ISSUE); |
93 | } | 152 | } |
@@ -96,16 +155,16 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
96 | @Check | 155 | @Check |
97 | public void checkUniqueDeclarations(Problem problem) { | 156 | public void checkUniqueDeclarations(Problem problem) { |
98 | var relations = new ArrayList<Relation>(); | 157 | var relations = new ArrayList<Relation>(); |
99 | var individuals = new ArrayList<Node>(); | 158 | var nodes = new ArrayList<Node>(); |
100 | for (var statement : problem.getStatements()) { | 159 | for (var statement : problem.getStatements()) { |
101 | if (statement instanceof Relation relation) { | 160 | if (statement instanceof Relation relation) { |
102 | relations.add(relation); | 161 | relations.add(relation); |
103 | } else if (statement instanceof IndividualDeclaration individualDeclaration) { | 162 | } else if (statement instanceof NodeDeclaration nodeDeclaration) { |
104 | individuals.addAll(individualDeclaration.getNodes()); | 163 | nodes.addAll(nodeDeclaration.getNodes()); |
105 | } | 164 | } |
106 | } | 165 | } |
107 | checkUniqueSimpleNames(relations); | 166 | checkUniqueSimpleNames(relations); |
108 | checkUniqueSimpleNames(individuals); | 167 | checkUniqueSimpleNames(nodes); |
109 | } | 168 | } |
110 | 169 | ||
111 | @Check | 170 | @Check |
@@ -302,6 +361,10 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
302 | @Check | 361 | @Check |
303 | public void checkTypeScope(TypeScope typeScope) { | 362 | public void checkTypeScope(TypeScope typeScope) { |
304 | checkArity(typeScope, ProblemPackage.Literals.TYPE_SCOPE__TARGET_TYPE, 1); | 363 | checkArity(typeScope, ProblemPackage.Literals.TYPE_SCOPE__TARGET_TYPE, 1); |
364 | if (typeScope.isIncrement() && ProblemUtil.isInModule(typeScope)) { | ||
365 | acceptError("Incremental type scopes are not supported in modules", typeScope, null, 0, | ||
366 | INVALID_MULTIPLICITY_ISSUE); | ||
367 | } | ||
305 | } | 368 | } |
306 | 369 | ||
307 | private void checkArity(EObject eObject, EReference reference, int expectedArity) { | 370 | private void checkArity(EObject eObject, EReference reference, int expectedArity) { |
@@ -351,9 +414,9 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
351 | } | 414 | } |
352 | 415 | ||
353 | private void checkExistsAssertion(Assertion assertion, LogicValue value) { | 416 | private void checkExistsAssertion(Assertion assertion, LogicValue value) { |
354 | if (value == LogicValue.TRUE || value == LogicValue.UNKNOWN) { | 417 | if (value == LogicValue.UNKNOWN) { |
355 | // {@code true} is always a valid value for {@code exists}, while {@code unknown} values may always be | 418 | // {@code unknown} values may always be refined to {@code true} of {@code false} if necessary (e.g., for |
356 | // refined to {@code true} if necessary (e.g., for individual nodes). | 419 | // atom nodes or removed multi-objects). |
357 | return; | 420 | return; |
358 | } | 421 | } |
359 | var arguments = assertion.getArguments(); | 422 | var arguments = assertion.getArguments(); |
@@ -361,9 +424,16 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
361 | // We already report an error on invalid arity. | 424 | // We already report an error on invalid arity. |
362 | return; | 425 | return; |
363 | } | 426 | } |
364 | var node = getNodeArgumentForMultiObjectAssertion(arguments.get(0)); | 427 | var node = getNodeArgumentForMultiObjectAssertion(arguments.getFirst()); |
365 | if (node != null && !node.eIsProxy() && ProblemUtil.isIndividualNode(node)) { | 428 | if (node == null || node.eIsProxy()) { |
366 | acceptError("Individual nodes must exist.", assertion, null, 0, UNSUPPORTED_ASSERTION_ISSUE); | 429 | return; |
430 | } | ||
431 | if (ProblemUtil.isAtomNode(node) && value != LogicValue.TRUE) { | ||
432 | acceptError("Atom nodes must exist.", assertion, null, 0, UNSUPPORTED_ASSERTION_ISSUE); | ||
433 | } | ||
434 | if (ProblemUtil.isMultiNode(node) && value != LogicValue.FALSE && ProblemUtil.isInModule(node)) { | ||
435 | acceptError("Multi-objects in modules cannot be required to exist.", assertion, null, 0, | ||
436 | UNSUPPORTED_ASSERTION_ISSUE); | ||
367 | } | 437 | } |
368 | } | 438 | } |
369 | 439 | ||
@@ -403,4 +473,23 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
403 | } | 473 | } |
404 | throw new IllegalArgumentException("Unknown assertion argument: " + argument); | 474 | throw new IllegalArgumentException("Unknown assertion argument: " + argument); |
405 | } | 475 | } |
476 | |||
477 | @Check | ||
478 | private void checkImplicitNodeInModule(Assertion assertion) { | ||
479 | if (!ProblemUtil.isInModule(assertion)) { | ||
480 | return; | ||
481 | } | ||
482 | for (var argument : assertion.getArguments()) { | ||
483 | if (argument instanceof NodeAssertionArgument nodeAssertionArgument) { | ||
484 | var node = nodeAssertionArgument.getNode(); | ||
485 | if (node != null && !node.eIsProxy() && ProblemUtil.isImplicitNode(node)) { | ||
486 | var name = node.getName(); | ||
487 | var message = ("Implicit nodes are not allowed in modules. Explicitly declare '%s' as a node " + | ||
488 | "with the declaration 'declare %s.'").formatted(name, name); | ||
489 | acceptError(message, nodeAssertionArgument, ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE, | ||
490 | 0, UNSUPPORTED_ASSERTION_ISSUE); | ||
491 | } | ||
492 | } | ||
493 | } | ||
494 | } | ||
406 | } | 495 | } |