aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language/src/main/java/tools/refinery/language
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/language/src/main/java/tools/refinery/language')
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/GenerateProblem.mwe22
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/Problem.xtext32
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java35
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java11
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java27
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/library/ClasspathBasedLibrary.java92
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/library/PathLibrary.java90
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java26
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/naming/NamingUtil.java33
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/naming/ProblemDelegateQualifiedNameProvider.java44
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameConverter.java2
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/naming/ProblemQualifiedNameProvider.java41
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/parser/ProblemEcoreElementFactory.java24
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/parser/antlr/IdentifierTokenProvider.java25
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/parser/antlr/ProblemTokenSource.java38
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java52
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemLocationInFileProvider.java4
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java27
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescription.java106
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionManager.java19
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java99
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/state/DerivedVariableComputer.java (renamed from subprojects/language/src/main/java/tools/refinery/language/resource/DerivedVariableComputer.java)2
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/state/ImplicitVariableScope.java (renamed from subprojects/language/src/main/java/tools/refinery/language/resource/ImplicitVariableScope.java)2
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/state/NodeNameCollector.java (renamed from subprojects/language/src/main/java/tools/refinery/language/resource/NodeNameCollector.java)2
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/state/ProblemDerivedStateComputer.java (renamed from subprojects/language/src/main/java/tools/refinery/language/resource/ProblemDerivedStateComputer.java)2
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/CompositeSelectable.java62
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/NoFullyQualifiedNamesSelectable.java62
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/NormalizedSelectable.java109
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemGlobalScopeProvider.java86
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java67
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/ShadowingKeyAwareSelectableBasedScope.java52
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/imports/Import.java12
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java205
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollection.java76
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java160
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/imports/NamedImport.java22
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/imports/TransitiveImport.java11
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/serializer/ProblemTransientValueService.java24
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/utils/ProblemDesugarer.java7
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java51
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java125
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
9import "https://refinery.tools/emf/2021/Problem" 9import "https://refinery.tools/emf/2021/Problem"
10 10
11Problem: 11Problem:
12 ("problem" name=Identifier ".")? 12 (kind=ModuleKind name=QualifiedName? ".")?
13 statements+=Statement*; 13 statements+=Statement*;
14 14
15enum ModuleKind:
16 PROBLEM="problem" | MODULE="module";
17
15Statement: 18Statement:
16 Assertion | ClassDeclaration | EnumDeclaration | 19 ImportStatement | Assertion | ClassDeclaration | EnumDeclaration |
17 PredicateDefinition | /* FunctionDefinition | RuleDefinition | */ 20 PredicateDefinition | /* FunctionDefinition | RuleDefinition | */
18 ScopeDeclaration | IndividualDeclaration; 21 ScopeDeclaration | NodeDeclaration;
22
23ImportStatement:
24 "import" importedModule=[Problem|QualifiedName] ("as" alias=ID)? ".";
19 25
20ClassDeclaration: 26ClassDeclaration:
21 abstract?="abstract"? "class" 27 abstract?="abstract"? "class"
@@ -252,23 +258,28 @@ RangeMultiplicity:
252ExactMultiplicity: 258ExactMultiplicity:
253 exactValue=INT; 259 exactValue=INT;
254 260
255IndividualDeclaration: 261NodeDeclaration:
256 "indiv" nodes+=EnumLiteral ("," nodes+=EnumLiteral)* "."; 262 ("declare" | "declare"? kind=NodeKind)
263 nodes+=EnumLiteral ("," nodes+=EnumLiteral)* ".";
264
265enum NodeKind:
266 ATOM="atom" | MULTI="multi";
257 267
258UpperBound returns ecore::EInt: 268UpperBound returns ecore::EInt:
259 INT | "*"; 269 INT | "*";
260 270
261QualifiedName hidden(): 271QualifiedName hidden():
262 Identifier ("::" Identifier)*; 272 "::"? Identifier (QUALIFIED_NAME_SEPARATOR Identifier)*;
263 273
264NonContainmentQualifiedName hidden(): 274NonContainmentQualifiedName hidden():
265 NonContainmentIdentifier ("::" Identifier)*; 275 (NonContainmentIdentifier | "::" Identifier) (QUALIFIED_NAME_SEPARATOR Identifier)*;
266 276
267Identifier: 277Identifier:
268 NonContainmentIdentifier | "contains" | "container"; 278 NonContainmentIdentifier | "contains" | "container";
269 279
270NonContainmentIdentifier: 280NonContainmentIdentifier:
271 ID | "contained" | "sum" | "prod" | "min" | "max"; 281 ID | "atom" | "multi" | "contained" |
282 "sum" | "prod" | "min" | "max" | "problem" | "module";
272 283
273Real returns ecore::EDouble: 284Real returns ecore::EDouble:
274 EXPONENTIAL | INT "." (INT | EXPONENTIAL); 285 EXPONENTIAL | INT "." (INT | EXPONENTIAL);
@@ -276,6 +287,9 @@ Real returns ecore::EDouble:
276terminal TRANSITIVE_CLOSURE: 287terminal TRANSITIVE_CLOSURE:
277 "synthetic:TRANSITIVE_CLOSURE"; 288 "synthetic:TRANSITIVE_CLOSURE";
278 289
290terminal QUALIFIED_NAME_SEPARATOR:
291 "synthetic::QUALIFIED_NAME_SEPARATOR";
292
279@Override 293@Override
280terminal ID: 294terminal 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;
14import org.eclipse.xtext.conversion.IValueConverterService; 14import org.eclipse.xtext.conversion.IValueConverterService;
15import org.eclipse.xtext.linking.ILinkingService; 15import org.eclipse.xtext.linking.ILinkingService;
16import org.eclipse.xtext.naming.IQualifiedNameConverter; 16import org.eclipse.xtext.naming.IQualifiedNameConverter;
17import org.eclipse.xtext.naming.IQualifiedNameProvider;
18import org.eclipse.xtext.parser.IAstFactory;
17import org.eclipse.xtext.parser.IParser; 19import org.eclipse.xtext.parser.IParser;
20import org.eclipse.xtext.parsetree.reconstr.ITransientValueService;
18import org.eclipse.xtext.resource.*; 21import org.eclipse.xtext.resource.*;
19import org.eclipse.xtext.scoping.IGlobalScopeProvider; 22import org.eclipse.xtext.scoping.IGlobalScopeProvider;
20import org.eclipse.xtext.scoping.IScopeProvider; 23import org.eclipse.xtext.scoping.IScopeProvider;
@@ -26,16 +29,21 @@ import org.eclipse.xtext.validation.IResourceValidator;
26import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; 29import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator;
27import tools.refinery.language.conversion.ProblemValueConverterService; 30import tools.refinery.language.conversion.ProblemValueConverterService;
28import tools.refinery.language.linking.ProblemLinkingService; 31import tools.refinery.language.linking.ProblemLinkingService;
32import tools.refinery.language.naming.ProblemDelegateQualifiedNameProvider;
29import tools.refinery.language.naming.ProblemQualifiedNameConverter; 33import tools.refinery.language.naming.ProblemQualifiedNameConverter;
34import tools.refinery.language.naming.ProblemQualifiedNameProvider;
35import tools.refinery.language.parser.ProblemEcoreElementFactory;
30import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; 36import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser;
31import tools.refinery.language.resource.ProblemDerivedStateComputer;
32import tools.refinery.language.resource.ProblemLocationInFileProvider; 37import tools.refinery.language.resource.ProblemLocationInFileProvider;
33import tools.refinery.language.resource.ProblemResource; 38import tools.refinery.language.resource.ProblemResource;
39import tools.refinery.language.resource.ProblemResourceDescriptionManager;
34import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; 40import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
41import tools.refinery.language.resource.state.ProblemDerivedStateComputer;
35import tools.refinery.language.scoping.ProblemGlobalScopeProvider; 42import tools.refinery.language.scoping.ProblemGlobalScopeProvider;
36import tools.refinery.language.scoping.ProblemLocalScopeProvider; 43import tools.refinery.language.scoping.ProblemLocalScopeProvider;
37import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; 44import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer;
38import tools.refinery.language.serializer.ProblemCrossReferenceSerializer; 45import tools.refinery.language.serializer.ProblemCrossReferenceSerializer;
46import tools.refinery.language.serializer.ProblemTransientValueService;
39import tools.refinery.language.validation.ProblemDiagnosticConverter; 47import 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 */
6package tools.refinery.language.library;
7
8import org.eclipse.emf.common.util.URI;
9import org.eclipse.xtext.naming.QualifiedName;
10
11import java.util.List;
12
13public 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 */
6package tools.refinery.language.library;
7
8import org.eclipse.emf.common.util.URI;
9import org.eclipse.xtext.naming.QualifiedName;
10
11import java.nio.file.Path;
12import java.util.*;
13
14public 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 */
6package tools.refinery.language.library;
7
8import org.eclipse.emf.common.util.URI;
9import org.eclipse.xtext.naming.QualifiedName;
10
11import java.nio.file.Files;
12import java.nio.file.Path;
13import java.util.ArrayList;
14import java.util.List;
15import java.util.Optional;
16
17public 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 */
6package tools.refinery.language.library;
7
8import org.eclipse.emf.common.util.URI;
9import org.eclipse.xtext.naming.QualifiedName;
10import tools.refinery.language.utils.ProblemUtil;
11
12import java.nio.file.Path;
13import java.util.List;
14import java.util.Optional;
15
16public 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 */
6package tools.refinery.language.naming; 6package tools.refinery.language.naming;
7 7
8import org.eclipse.xtext.naming.QualifiedName;
9
8import java.util.regex.Pattern; 10import java.util.regex.Pattern;
9 11
10public final class NamingUtil { 12public 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 */
6package tools.refinery.language.naming;
7
8import com.google.inject.Inject;
9import org.eclipse.xtext.naming.DefaultDeclarativeQualifiedNameProvider;
10import org.eclipse.xtext.naming.IQualifiedNameConverter;
11import org.eclipse.xtext.naming.QualifiedName;
12import tools.refinery.language.model.problem.Problem;
13import tools.refinery.language.scoping.imports.ImportAdapter;
14import tools.refinery.language.utils.ProblemUtil;
15
16public 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
13public class ProblemQualifiedNameConverter extends IQualifiedNameConverter.DefaultImpl { 13public 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 */
6package tools.refinery.language.naming;
7
8import com.google.inject.Inject;
9import com.google.inject.name.Named;
10import org.eclipse.emf.ecore.EObject;
11import org.eclipse.xtext.naming.IQualifiedNameProvider;
12import org.eclipse.xtext.naming.QualifiedName;
13import org.eclipse.xtext.util.IResourceScopeCache;
14import org.eclipse.xtext.util.Tuples;
15import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
16
17public 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 */
6package tools.refinery.language.parser;
7
8import org.eclipse.emf.ecore.EObject;
9import org.eclipse.xtext.conversion.ValueConverterException;
10import org.eclipse.xtext.nodemodel.INode;
11import org.eclipse.xtext.parser.DefaultEcoreElementFactory;
12import tools.refinery.language.model.problem.Problem;
13import tools.refinery.language.model.problem.ProblemPackage;
14
15public 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 */
6package tools.refinery.language.resource;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.common.util.URI;
10import org.eclipse.emf.ecore.resource.Resource;
11import org.eclipse.emf.ecore.util.EcoreUtil;
12import org.eclipse.xtext.EcoreUtil2;
13import org.eclipse.xtext.resource.IResourceDescription;
14import org.eclipse.xtext.resource.IResourceDescriptions;
15import org.eclipse.xtext.resource.IResourceDescriptionsProvider;
16import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider;
17
18public 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;
20import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic; 20import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic;
21import org.eclipse.xtext.linking.lazy.LazyLinkingResource; 21import org.eclipse.xtext.linking.lazy.LazyLinkingResource;
22import org.eclipse.xtext.nodemodel.INode; 22import org.eclipse.xtext.nodemodel.INode;
23import org.eclipse.xtext.parser.IParseResult;
23import org.eclipse.xtext.resource.DerivedStateAwareResource; 24import org.eclipse.xtext.resource.DerivedStateAwareResource;
24import org.eclipse.xtext.util.Triple; 25import org.eclipse.xtext.util.Triple;
25import org.jetbrains.annotations.Nullable; 26import org.jetbrains.annotations.Nullable;
27import tools.refinery.language.model.problem.Problem;
28import tools.refinery.language.utils.ProblemUtil;
26 29
27import java.util.Arrays; 30import java.util.Arrays;
28import java.util.List; 31import 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 *******************************************************************************/
9package tools.refinery.language.resource;
10
11import org.apache.log4j.Logger;
12import org.eclipse.emf.common.util.TreeIterator;
13import org.eclipse.emf.ecore.EObject;
14import org.eclipse.emf.ecore.resource.Resource;
15import org.eclipse.emf.ecore.util.EcoreUtil;
16import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy;
17import org.eclipse.xtext.resource.IEObjectDescription;
18import org.eclipse.xtext.resource.impl.DefaultResourceDescription;
19import org.eclipse.xtext.resource.impl.EObjectDescriptionLookUp;
20import org.eclipse.xtext.util.IAcceptor;
21import tools.refinery.language.naming.NamingUtil;
22
23import java.io.IOException;
24import 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 */
32public 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 */
6package tools.refinery.language.resource;
7
8import org.eclipse.emf.ecore.resource.Resource;
9import org.eclipse.xtext.resource.DerivedStateAwareResourceDescriptionManager;
10import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy;
11import org.eclipse.xtext.resource.IResourceDescription;
12
13public 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;
8import com.google.common.collect.ImmutableMap; 8import com.google.common.collect.ImmutableMap;
9import com.google.inject.Inject; 9import com.google.inject.Inject;
10import com.google.inject.Singleton; 10import com.google.inject.Singleton;
11import com.google.inject.name.Named;
11import org.eclipse.emf.ecore.EObject; 12import org.eclipse.emf.ecore.EObject;
12import org.eclipse.xtext.EcoreUtil2; 13import org.eclipse.xtext.EcoreUtil2;
13import org.eclipse.xtext.naming.IQualifiedNameConverter; 14import org.eclipse.xtext.naming.IQualifiedNameConverter;
15import org.eclipse.xtext.naming.IQualifiedNameProvider;
14import org.eclipse.xtext.naming.QualifiedName; 16import org.eclipse.xtext.naming.QualifiedName;
15import org.eclipse.xtext.resource.EObjectDescription; 17import org.eclipse.xtext.resource.EObjectDescription;
16import org.eclipse.xtext.resource.IEObjectDescription; 18import org.eclipse.xtext.resource.IEObjectDescription;
17import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; 19import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy;
18import org.eclipse.xtext.util.IAcceptor; 20import org.eclipse.xtext.util.IAcceptor;
21import tools.refinery.language.naming.ProblemQualifiedNameProvider;
22import tools.refinery.language.scoping.imports.ImportCollector;
19import tools.refinery.language.model.problem.*; 23import tools.refinery.language.model.problem.*;
20import tools.refinery.language.naming.NamingUtil; 24import tools.refinery.language.naming.NamingUtil;
21import tools.refinery.language.utils.ProblemUtil; 25import tools.refinery.language.utils.ProblemUtil;
22 26
23import java.util.Map; 27import java.util.Map;
28import java.util.stream.Collectors;
24 29
25@Singleton 30@Singleton
26public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { 31public 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 */
6package tools.refinery.language.resource; 6package tools.refinery.language.resource.state;
7 7
8import com.google.inject.Inject; 8import com.google.inject.Inject;
9import com.google.inject.Singleton; 9import 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 */
6package tools.refinery.language.resource; 6package tools.refinery.language.resource.state;
7 7
8import org.eclipse.emf.ecore.EObject; 8import org.eclipse.emf.ecore.EObject;
9import org.eclipse.xtext.linking.impl.LinkingHelper; 9import 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 */
6package tools.refinery.language.resource; 6package tools.refinery.language.resource.state;
7 7
8import com.google.common.collect.ImmutableSet; 8import com.google.common.collect.ImmutableSet;
9import com.google.inject.Inject; 9import 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 */
6package tools.refinery.language.resource; 6package tools.refinery.language.resource.state;
7 7
8import com.google.inject.Inject; 8import com.google.inject.Inject;
9import com.google.inject.Provider; 9import 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 */
6package tools.refinery.language.scoping;
7
8import com.google.common.collect.Iterables;
9import org.eclipse.emf.ecore.EClass;
10import org.eclipse.emf.ecore.EObject;
11import org.eclipse.xtext.naming.QualifiedName;
12import org.eclipse.xtext.resource.IEObjectDescription;
13import org.eclipse.xtext.resource.ISelectable;
14
15import java.util.Collection;
16import java.util.List;
17
18class 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 */
6package tools.refinery.language.scoping;
7
8import com.google.common.base.Predicate;
9import com.google.common.collect.Iterables;
10import org.eclipse.emf.ecore.EClass;
11import org.eclipse.emf.ecore.EObject;
12import org.eclipse.xtext.naming.QualifiedName;
13import org.eclipse.xtext.resource.IEObjectDescription;
14import org.eclipse.xtext.resource.ISelectable;
15import tools.refinery.language.naming.NamingUtil;
16
17import java.util.List;
18
19public 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 */
6package tools.refinery.language.scoping;
7
8import com.google.common.collect.Iterables;
9import org.eclipse.emf.ecore.EClass;
10import org.eclipse.emf.ecore.EObject;
11import org.eclipse.xtext.naming.QualifiedName;
12import org.eclipse.xtext.resource.IEObjectDescription;
13import org.eclipse.xtext.resource.ISelectable;
14import org.eclipse.xtext.resource.impl.AliasedEObjectDescription;
15
16import java.util.Iterator;
17import java.util.List;
18import java.util.NoSuchElementException;
19
20class 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 */
6package tools.refinery.language.scoping; 6package tools.refinery.language.scoping;
7 7
8import java.util.LinkedHashSet; 8import com.google.common.base.Predicate;
9 9import com.google.inject.Inject;
10import org.eclipse.emf.common.util.URI; 10import com.google.inject.Provider;
11import org.eclipse.emf.ecore.EClass;
11import org.eclipse.emf.ecore.resource.Resource; 12import org.eclipse.emf.ecore.resource.Resource;
12import org.eclipse.xtext.scoping.impl.ImportUriGlobalScopeProvider; 13import org.eclipse.xtext.naming.QualifiedName;
14import org.eclipse.xtext.resource.IEObjectDescription;
15import org.eclipse.xtext.resource.ISelectable;
16import org.eclipse.xtext.scoping.IScope;
17import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeProvider;
18import org.eclipse.xtext.util.IResourceScopeCache;
19import tools.refinery.language.resource.LoadOnDemandResourceDescriptionProvider;
20import tools.refinery.language.scoping.imports.ImportCollector;
21import tools.refinery.language.scoping.imports.NamedImport;
22
23import java.util.ArrayList;
24import java.util.Collection;
25import java.util.List;
26
27public class ProblemGlobalScopeProvider extends AbstractGlobalScopeProvider {
28 private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemGlobalScopeProvider.CACHE_KEY";
13 29
14import tools.refinery.language.utils.ProblemUtil; 30 @Inject
31 private ImportCollector importCollector;
15 32
16public 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 */
6package tools.refinery.language.scoping; 6package tools.refinery.language.scoping;
7 7
8import java.util.List; 8import com.google.inject.Inject;
9 9import com.google.inject.name.Named;
10import org.eclipse.emf.ecore.EObject; 10import org.eclipse.emf.ecore.EObject;
11import org.eclipse.emf.ecore.EReference;
11import org.eclipse.emf.ecore.resource.Resource; 12import org.eclipse.emf.ecore.resource.Resource;
13import org.eclipse.xtext.naming.IQualifiedNameProvider;
12import org.eclipse.xtext.naming.QualifiedName; 14import org.eclipse.xtext.naming.QualifiedName;
13import org.eclipse.xtext.resource.IResourceDescriptions;
14import org.eclipse.xtext.resource.IResourceDescriptionsProvider; 15import org.eclipse.xtext.resource.IResourceDescriptionsProvider;
15import org.eclipse.xtext.resource.ISelectable; 16import org.eclipse.xtext.resource.ISelectable;
16import org.eclipse.xtext.scoping.impl.ImportNormalizer; 17import org.eclipse.xtext.scoping.IScope;
17import org.eclipse.xtext.scoping.impl.ImportedNamespaceAwareLocalScopeProvider; 18import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeDelegatingScopeProvider;
19import org.eclipse.xtext.util.IResourceScopeCache;
20import tools.refinery.language.naming.ProblemQualifiedNameProvider;
18 21
19import com.google.inject.Inject; 22public class ProblemLocalScopeProvider extends AbstractGlobalScopeDelegatingScopeProvider {
23 private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemLocalScopeProvider.CACHE_KEY";
20 24
21import tools.refinery.language.utils.ProblemUtil; 25 @Inject
22 26 @Named(ProblemQualifiedNameProvider.NAMED_DELEGATE)
23public 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 */
6package tools.refinery.language.scoping;
7
8import com.google.common.base.Predicate;
9import org.eclipse.emf.ecore.EClass;
10import org.eclipse.xtext.resource.IEObjectDescription;
11import org.eclipse.xtext.resource.ISelectable;
12import org.eclipse.xtext.scoping.IScope;
13import org.eclipse.xtext.scoping.impl.SelectableBasedScope;
14import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
15
16import java.util.Objects;
17
18public 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 */
6package tools.refinery.language.scoping.imports;
7
8import org.eclipse.emf.common.util.URI;
9
10public 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 */
6package tools.refinery.language.scoping.imports;
7
8import com.google.common.base.Splitter;
9import com.google.common.cache.Cache;
10import com.google.common.cache.CacheBuilder;
11import org.apache.log4j.Logger;
12import org.eclipse.emf.common.notify.Notification;
13import org.eclipse.emf.common.notify.impl.AdapterImpl;
14import org.eclipse.emf.common.util.URI;
15import org.eclipse.emf.ecore.resource.Resource;
16import org.eclipse.emf.ecore.resource.ResourceSet;
17import org.eclipse.emf.ecore.util.EcoreUtil;
18import org.eclipse.xtext.naming.QualifiedName;
19import tools.refinery.language.library.RefineryLibrary;
20
21import java.io.File;
22import java.nio.file.Path;
23import java.util.*;
24
25public 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 */
6package tools.refinery.language.scoping.imports;
7
8import org.eclipse.emf.common.util.URI;
9import org.jetbrains.annotations.NotNull;
10import org.jetbrains.annotations.Nullable;
11
12import java.util.*;
13
14public 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 */
6package tools.refinery.language.scoping.imports;
7
8import com.google.common.base.Splitter;
9import com.google.common.base.Strings;
10import com.google.inject.Inject;
11import com.google.inject.Provider;
12import com.google.inject.Singleton;
13import org.eclipse.emf.common.util.URI;
14import org.eclipse.emf.ecore.EObject;
15import org.eclipse.emf.ecore.resource.Resource;
16import org.eclipse.xtext.linking.impl.LinkingHelper;
17import org.eclipse.xtext.naming.IQualifiedNameConverter;
18import org.eclipse.xtext.naming.QualifiedName;
19import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
20import org.eclipse.xtext.resource.IEObjectDescription;
21import org.eclipse.xtext.util.IResourceScopeCache;
22import tools.refinery.language.model.problem.ImportStatement;
23import tools.refinery.language.model.problem.Problem;
24import tools.refinery.language.model.problem.ProblemPackage;
25import tools.refinery.language.naming.NamingUtil;
26import tools.refinery.language.resource.LoadOnDemandResourceDescriptionProvider;
27import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
28
29import java.util.*;
30
31@Singleton
32public 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 */
6package tools.refinery.language.scoping.imports;
7
8import org.eclipse.emf.common.util.URI;
9import org.eclipse.xtext.naming.QualifiedName;
10
11import java.util.List;
12
13public 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 */
6package tools.refinery.language.scoping.imports;
7
8import org.eclipse.emf.common.util.URI;
9
10public 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 */
6package tools.refinery.language.serializer;
7
8import org.eclipse.emf.ecore.EObject;
9import org.eclipse.emf.ecore.EStructuralFeature;
10import org.eclipse.xtext.parsetree.reconstr.impl.DefaultTransientValueService;
11import tools.refinery.language.model.problem.Problem;
12import tools.refinery.language.model.problem.ProblemPackage;
13import tools.refinery.language.utils.ProblemUtil;
14
15public 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;
11import org.eclipse.emf.ecore.resource.Resource; 11import org.eclipse.emf.ecore.resource.Resource;
12import org.eclipse.xtext.util.IResourceScopeCache; 12import org.eclipse.xtext.util.IResourceScopeCache;
13import org.eclipse.xtext.util.Tuples; 13import org.eclipse.xtext.util.Tuples;
14import tools.refinery.language.library.BuiltinLibrary;
14import tools.refinery.language.model.problem.*; 15import tools.refinery.language.model.problem.*;
15 16
16import java.util.*; 17import 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;
8import org.eclipse.emf.common.util.URI; 8import org.eclipse.emf.common.util.URI;
9import org.eclipse.emf.ecore.EObject; 9import org.eclipse.emf.ecore.EObject;
10import org.eclipse.emf.ecore.util.EcoreUtil; 10import org.eclipse.emf.ecore.util.EcoreUtil;
11import org.eclipse.xtext.EcoreUtil2;
12import tools.refinery.language.library.BuiltinLibrary;
11import tools.refinery.language.model.problem.*; 13import tools.refinery.language.model.problem.*;
12 14
13public final class ProblemUtil { 15public 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;
13import org.eclipse.emf.ecore.EObject; 13import org.eclipse.emf.ecore.EObject;
14import org.eclipse.emf.ecore.EReference; 14import org.eclipse.emf.ecore.EReference;
15import org.eclipse.xtext.EcoreUtil2; 15import org.eclipse.xtext.EcoreUtil2;
16import org.eclipse.xtext.naming.IQualifiedNameConverter;
16import org.eclipse.xtext.validation.Check; 17import org.eclipse.xtext.validation.Check;
17import org.jetbrains.annotations.Nullable; 18import org.jetbrains.annotations.Nullable;
18import tools.refinery.language.model.problem.*; 19import tools.refinery.language.model.problem.*;
20import tools.refinery.language.naming.NamingUtil;
21import tools.refinery.language.scoping.imports.ImportAdapter;
19import tools.refinery.language.utils.ProblemDesugarer; 22import tools.refinery.language.utils.ProblemDesugarer;
20import tools.refinery.language.utils.ProblemUtil; 23import tools.refinery.language.utils.ProblemUtil;
21 24
22import java.util.ArrayList; 25import java.util.*;
23import java.util.LinkedHashMap;
24import java.util.LinkedHashSet;
25import 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;
33public class ProblemValidator extends AbstractProblemValidator { 33public 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}