aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/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
-rw-r--r--subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary5
-rw-r--r--subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery (renamed from subprojects/language/src/main/resources/tools/refinery/language/builtin.problem)3
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java2
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java8
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java82
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java18
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java1
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/ModuleValidationTest.java145
-rw-r--r--subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java5
50 files changed, 2024 insertions, 213 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}
diff --git a/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary b/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary
new file mode 100644
index 00000000..8e454ee5
--- /dev/null
+++ b/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary
@@ -0,0 +1,5 @@
1# SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
2#
3# SPDX-License-Identifier: EPL-2.0
4tools.refinery.language.library.BuiltinLibrary
5tools.refinery.language.library.PathLibrary
diff --git a/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem b/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery
index 022c3167..4e74ca03 100644
--- a/subprojects/language/src/main/resources/tools/refinery/language/builtin.problem
+++ b/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery
@@ -1,7 +1,6 @@
1% SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> 1% SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/>
2% 2%
3% SPDX-License-Identifier: EPL-2.0 3% SPDX-License-Identifier: EPL-2.0
4problem builtin.
5 4
6abstract class node. 5abstract class node.
7 6
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
index 72d57f54..17ae5fbb 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
@@ -42,7 +42,7 @@ class ProblemParsingTest {
42 error invalidTaxStatus(Person p) <-> 42 error invalidTaxStatus(Person p) <->
43 taxStatus(p, CHILD), children(p, _q). 43 taxStatus(p, CHILD), children(p, _q).
44 44
45 indiv family. 45 atom family.
46 Family(family). 46 Family(family).
47 members(family, anne): true. 47 members(family, anne): true.
48 members(family, bob). 48 members(family, bob).
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java
index f688d970..4a15f9de 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java
@@ -183,8 +183,8 @@ class ProblemFormatterTest {
183 } 183 }
184 184
185 @Test 185 @Test
186 void individualDeclarationTest() { 186 void atomDeclarationTest() {
187 testFormatter(" indiv a , b . ", "indiv a, b.\n"); 187 testFormatter(" atom a , b . ", "atom a, b.\n");
188 } 188 }
189 189
190 @Test 190 @Test
@@ -194,7 +194,7 @@ class ProblemFormatterTest {
194 pred foo(node a). 194 pred foo(node a).
195 class Foo. 195 class Foo.
196 foo(n1, n2). 196 foo(n1, n2).
197 indiv i1. 197 atom i1.
198 !foo(i1, n1). 198 !foo(i1, n1).
199 pred bar(node a, node b). 199 pred bar(node a, node b).
200 pred quux(). 200 pred quux().
@@ -207,7 +207,7 @@ class ProblemFormatterTest {
207 class Foo. 207 class Foo.
208 208
209 foo(n1, n2). 209 foo(n1, n2).
210 indiv i1. 210 atom i1.
211 !foo(i1, n1). 211 !foo(i1, n1).
212 212
213 pred bar(node a, node b). 213 pred bar(node a, node b).
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java
index e76d2993..0704e026 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.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 */
@@ -68,37 +68,31 @@ class NodeScopingTest {
68 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b"))); 68 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b")));
69 } 69 }
70 70
71 @ParameterizedTest 71 @Test
72 @MethodSource("individualNodeReferenceSource") 72 void atomNodeInAssertionTest() {
73 void individualNodeInAssertionTest(String qualifiedNamePrefix, boolean namedProblem) {
74 var problem = parse(""" 73 var problem = parse("""
75 indiv a, b. 74 atom a, b.
76 pred predicate(node x, node y) <-> node(x). 75 pred predicate(node x, node y) <-> node(x).
77 predicate({PARAM}a, {PARAM}a). 76 predicate(a, a).
78 ?predicate({PARAM}a, {PARAM}b). 77 ?predicate(a, b).
79 """, qualifiedNamePrefix, namedProblem); 78 """);
80 assertThat(problem.getResourceErrors(), empty()); 79 assertThat(problem.getResourceErrors(), empty());
81 assertThat(problem.nodeNames(), empty()); 80 assertThat(problem.nodeNames(), empty());
82 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a"))); 81 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.atomNode("a")));
83 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a"))); 82 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.atomNode("a")));
84 assertThat(problem.assertion(1).arg(0).node(), equalTo(problem.individualNode("a"))); 83 assertThat(problem.assertion(1).arg(0).node(), equalTo(problem.atomNode("a")));
85 assertThat(problem.assertion(1).arg(1).node(), equalTo(problem.individualNode("b"))); 84 assertThat(problem.assertion(1).arg(1).node(), equalTo(problem.atomNode("b")));
86 } 85 }
87 86
88 @ParameterizedTest 87 @Test
89 @MethodSource("individualNodeReferenceSource") 88 void atomNodeInPredicateTest() {
90 void individualNodeInPredicateTest(String qualifiedNamePrefix, boolean namedProblem) {
91 var problem = parse(""" 89 var problem = parse("""
92 indiv b. 90 atom b.
93 pred predicate(node a) <-> node({PARAM}b). 91 pred predicate(node a) <-> node(b).
94 """); 92 """);
95 assertThat(problem.getResourceErrors(), empty()); 93 assertThat(problem.getResourceErrors(), empty());
96 assertThat(problem.nodeNames(), empty()); 94 assertThat(problem.nodeNames(), empty());
97 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b"))); 95 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.atomNode("b")));
98 }
99
100 static Stream<Arguments> individualNodeReferenceSource() {
101 return Stream.of(Arguments.of("", false), Arguments.of("", true), Arguments.of("test::", true));
102 } 96 }
103 97
104 @Disabled("No nodes are present in builtin.problem currently") 98 @Disabled("No nodes are present in builtin.problem currently")
@@ -131,37 +125,30 @@ class NodeScopingTest {
131 return Stream.of(Arguments.of("int::new"), Arguments.of("builtin::int::new")); 125 return Stream.of(Arguments.of("int::new"), Arguments.of("builtin::int::new"));
132 } 126 }
133 127
134 @ParameterizedTest 128 @Test
135 @MethodSource("classNewNodeReferencesSource") 129 void classNewNodeTest() {
136 void classNewNodeTest(String qualifiedName, boolean namedProblem) {
137 var problem = parse(""" 130 var problem = parse("""
138 class Foo. 131 class Foo.
139 pred predicate(node x) <-> node(x). 132 pred predicate(node x) <-> node(x).
140 predicate({PARAM}). 133 predicate(Foo::new).
141 """, qualifiedName, namedProblem); 134 """);
142 assertThat(problem.getResourceErrors(), empty()); 135 assertThat(problem.getResourceErrors(), empty());
143 assertThat(problem.nodeNames(), empty()); 136 assertThat(problem.nodeNames(), empty());
144 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode())); 137 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode()));
145 } 138 }
146 139
147 @ParameterizedTest 140 @Test
148 @MethodSource("classNewNodeReferencesSource") 141 void classNewNodeInPredicateTest() {
149 void classNewNodeInPredicateTest(String qualifiedName, boolean namedProblem) {
150 var problem = parse(""" 142 var problem = parse("""
151 class Foo. 143 class Foo.
152 pred predicate(node x) <-> node({PARAM}). 144 pred predicate(node x) <-> node(Foo::new).
153 """, qualifiedName, namedProblem); 145 """);
154 assertThat(problem.getResourceErrors(), empty()); 146 assertThat(problem.getResourceErrors(), empty());
155 assertThat(problem.nodeNames(), empty()); 147 assertThat(problem.nodeNames(), empty());
156 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 148 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
157 equalTo(problem.findClass("Foo").get().getNewNode())); 149 equalTo(problem.findClass("Foo").get().getNewNode()));
158 } 150 }
159 151
160 static Stream<Arguments> classNewNodeReferencesSource() {
161 return Stream.of(Arguments.of("Foo::new", false), Arguments.of("Foo::new", true),
162 Arguments.of("test::Foo::new", true));
163 }
164
165 @Test 152 @Test
166 void newNodeIsNotSpecial() { 153 void newNodeIsNotSpecial() {
167 var problem = parse(""" 154 var problem = parse("""
@@ -176,12 +163,12 @@ class NodeScopingTest {
176 163
177 @ParameterizedTest 164 @ParameterizedTest
178 @MethodSource("enumLiteralReferencesSource") 165 @MethodSource("enumLiteralReferencesSource")
179 void enumLiteralTest(String qualifiedName, boolean namedProblem) { 166 void enumLiteralTest(String qualifiedName) {
180 var problem = parse(""" 167 var problem = parse("""
181 enum Foo { alpha, beta } 168 enum Foo { alpha, beta }
182 pred predicate(Foo a) <-> node(a). 169 pred predicate(Foo a) <-> node(a).
183 predicate({PARAM}). 170 predicate({PARAM}).
184 """, qualifiedName, namedProblem); 171 """, qualifiedName);
185 assertThat(problem.getResourceErrors(), empty()); 172 assertThat(problem.getResourceErrors(), empty());
186 assertThat(problem.nodeNames(), empty()); 173 assertThat(problem.nodeNames(), empty());
187 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha"))); 174 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha")));
@@ -189,11 +176,11 @@ class NodeScopingTest {
189 176
190 @ParameterizedTest 177 @ParameterizedTest
191 @MethodSource("enumLiteralReferencesSource") 178 @MethodSource("enumLiteralReferencesSource")
192 void enumLiteralInPredicateTest(String qualifiedName, boolean namedProblem) { 179 void enumLiteralInPredicateTest(String qualifiedName) {
193 var problem = parse(""" 180 var problem = parse("""
194 enum Foo { alpha, beta } 181 enum Foo { alpha, beta }
195 pred predicate(Foo a) <-> node({PARAM}). 182 pred predicate(Foo a) <-> node({PARAM}).
196 """, qualifiedName, namedProblem); 183 """, qualifiedName);
197 assertThat(problem.getResourceErrors(), empty()); 184 assertThat(problem.getResourceErrors(), empty());
198 assertThat(problem.nodeNames(), empty()); 185 assertThat(problem.nodeNames(), empty());
199 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 186 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
@@ -201,9 +188,7 @@ class NodeScopingTest {
201 } 188 }
202 189
203 static Stream<Arguments> enumLiteralReferencesSource() { 190 static Stream<Arguments> enumLiteralReferencesSource() {
204 return Stream.of(Arguments.of("alpha", false), Arguments.of("alpha", true), Arguments.of("Foo::alpha", false), 191 return Stream.of(Arguments.of("alpha"), Arguments.of("Foo::alpha"));
205 Arguments.of("Foo::alpha", true), Arguments.of("test::alpha", true),
206 Arguments.of("test::Foo::alpha", true));
207 } 192 }
208 193
209 @Disabled("No enum literals are present in builtin.problem currently") 194 @Disabled("No enum literals are present in builtin.problem currently")
@@ -222,7 +207,7 @@ class NodeScopingTest {
222 @Disabled("No enum literals are present in builtin.problem currently") 207 @Disabled("No enum literals are present in builtin.problem currently")
223 @ParameterizedTest 208 @ParameterizedTest
224 @MethodSource("builtInEnumLiteralReferencesSource") 209 @MethodSource("builtInEnumLiteralReferencesSource")
225 void bultInEnumLiteralInPredicateTest(String qualifiedName) { 210 void builtInEnumLiteralInPredicateTest(String qualifiedName) {
226 var problem = parse(""" 211 var problem = parse("""
227 pred predicate() <-> node({PARAM}). 212 pred predicate() <-> node({PARAM}).
228 """, qualifiedName); 213 """, qualifiedName);
@@ -237,13 +222,8 @@ class NodeScopingTest {
237 Arguments.of("builtin::bool::true")); 222 Arguments.of("builtin::bool::true"));
238 } 223 }
239 224
240 private WrappedProblem parse(String text, String parameter, boolean namedProblem) {
241 var problemName = namedProblem ? "problem test.\n" : "";
242 return parseHelper.parse(problemName + text.replace("{PARAM}", parameter));
243 }
244
245 private WrappedProblem parse(String text, String parameter) { 225 private WrappedProblem parse(String text, String parameter) {
246 return parse(text, parameter, false); 226 return parseHelper.parse(text.replace("{PARAM}", parameter));
247 } 227 }
248 228
249 private WrappedProblem parse(String text) { 229 private WrappedProblem parse(String text) {
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
index 65675b6b..ad583f8e 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
@@ -56,15 +56,16 @@ class ProblemSerializerTest {
56 var pred = createPred(); 56 var pred = createPred();
57 var node = ProblemFactory.eINSTANCE.createNode(); 57 var node = ProblemFactory.eINSTANCE.createNode();
58 node.setName("a"); 58 node.setName("a");
59 var individualDeclaration = ProblemFactory.eINSTANCE.createIndividualDeclaration(); 59 var atomDeclaration = ProblemFactory.eINSTANCE.createNodeDeclaration();
60 individualDeclaration.getNodes().add(node); 60 atomDeclaration.setKind(NodeKind.ATOM);
61 problem.getStatements().add(individualDeclaration); 61 atomDeclaration.getNodes().add(node);
62 problem.getStatements().add(atomDeclaration);
62 createAssertion(pred, node, value); 63 createAssertion(pred, node, value);
63 64
64 assertSerializedResult(""" 65 assertSerializedResult("""
65 pred foo(node p). 66 pred foo(node p).
66 67
67 indiv a. 68 atom a.
68 """ + serializedAssertion + "\n"); 69 """ + serializedAssertion + "\n");
69 } 70 }
70 71
@@ -79,15 +80,16 @@ class ProblemSerializerTest {
79 var pred = createPred(); 80 var pred = createPred();
80 var node = ProblemFactory.eINSTANCE.createNode(); 81 var node = ProblemFactory.eINSTANCE.createNode();
81 node.setName("a"); 82 node.setName("a");
82 var individualDeclaration = ProblemFactory.eINSTANCE.createIndividualDeclaration(); 83 var atomDeclaration = ProblemFactory.eINSTANCE.createNodeDeclaration();
83 individualDeclaration.getNodes().add(node); 84 atomDeclaration.setKind(NodeKind.ATOM);
84 problem.getStatements().add(individualDeclaration); 85 atomDeclaration.getNodes().add(node);
86 problem.getStatements().add(atomDeclaration);
85 createAssertion(pred, node, value, true); 87 createAssertion(pred, node, value, true);
86 88
87 assertSerializedResult(""" 89 assertSerializedResult("""
88 pred foo(node p). 90 pred foo(node p).
89 91
90 indiv a. 92 atom a.
91 default\040""" + serializedAssertion + "\n"); 93 default\040""" + serializedAssertion + "\n");
92 } 94 }
93 95
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java
index 82dea31b..1fb08845 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java
@@ -82,7 +82,6 @@ class AssertionValidationTest {
82 "exists(n).", 82 "exists(n).",
83 "?exists(n).", 83 "?exists(n).",
84 "!exists(n).", 84 "!exists(n).",
85 "exists(*).",
86 "?exists(*).", 85 "?exists(*).",
87 "exists(Foo::new).", 86 "exists(Foo::new).",
88 "?exists(Foo::new).", 87 "?exists(Foo::new).",
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ModuleValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ModuleValidationTest.java
new file mode 100644
index 00000000..00ad051b
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ModuleValidationTest.java
@@ -0,0 +1,145 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.tests.validation;
7
8import com.google.inject.Inject;
9import org.eclipse.xtext.testing.InjectWith;
10import org.eclipse.xtext.testing.extensions.InjectionExtension;
11import org.junit.jupiter.api.Test;
12import org.junit.jupiter.api.extension.ExtendWith;
13import org.junit.jupiter.params.ParameterizedTest;
14import org.junit.jupiter.params.provider.Arguments;
15import org.junit.jupiter.params.provider.MethodSource;
16import tools.refinery.language.model.tests.utils.ProblemParseHelper;
17import tools.refinery.language.tests.ProblemInjectorProvider;
18import tools.refinery.language.validation.ProblemValidator;
19
20import java.util.stream.Stream;
21
22import static org.hamcrest.MatcherAssert.assertThat;
23import static org.hamcrest.Matchers.*;
24
25@ExtendWith(InjectionExtension.class)
26@InjectWith(ProblemInjectorProvider.class)
27class ModuleValidationTest {
28 @Inject
29 private ProblemParseHelper parseHelper;
30
31 @ParameterizedTest
32 @MethodSource
33 void invalidMultiObjectExistsTest(String invalidDeclaration) {
34 var problem = parseHelper.parse("""
35 module.
36
37 %s
38 """.formatted(invalidDeclaration));
39 var issues = problem.validate();
40 assertThat(issues, hasItem(hasProperty("issueCode",
41 is(ProblemValidator.UNSUPPORTED_ASSERTION_ISSUE))));
42 }
43
44 static Stream<Arguments> invalidMultiObjectExistsTest() {
45 return Stream.of(
46 Arguments.of("""
47 class Foo.
48 exists(Foo::new).
49 """),
50 Arguments.of("""
51 multi m.
52 exists(m).
53 """));
54 }
55
56 @Test
57 void invalidScopeTest() {
58 var problem = parseHelper.parse("""
59 module.
60
61 class Foo.
62 scope Foo += 1.
63 """);
64 var issues = problem.validate();
65 assertThat(issues, hasItem(hasProperty("issueCode",
66 is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE))));
67 }
68
69 @Test
70 void invalidAssertionArgumentTest() {
71 var problem = parseHelper.parse("""
72 module.
73
74 class Foo.
75 Foo(foo1).
76 """);
77 var issues = problem.validate();
78 assertThat(issues, hasItem(allOf(
79 hasProperty("issueCode", is(ProblemValidator.UNSUPPORTED_ASSERTION_ISSUE)),
80 hasProperty("message", containsString("foo1")))));
81 }
82
83 @ParameterizedTest
84 @MethodSource
85 void validDeclarationTest(String validDeclaration) {
86 var problem = parseHelper.parse(validDeclaration);
87 var issues = problem.validate();
88 assertThat(issues, hasSize(0));
89 }
90
91 static Stream<Arguments> validDeclarationTest() {
92 return Stream.concat(
93 invalidMultiObjectExistsTest(),
94 Stream.of(
95 Arguments.of("""
96 class Foo.
97 scope Foo += 1.
98 """),
99 Arguments.of("""
100 module.
101
102 class Foo.
103 scope Foo = 1.
104 """),
105 Arguments.of("""
106 class Foo.
107 Foo(foo1).
108 """),
109 Arguments.of("""
110 module.
111
112 class Foo.
113 multi foo1.
114 Foo(foo1).
115 """),
116 Arguments.of("""
117 module.
118
119 class Foo.
120 atom foo1.
121 Foo(foo1).
122 """),
123 Arguments.of("""
124 module.
125
126 class Foo.
127 declare foo1.
128 Foo(foo1).
129 """),
130 Arguments.of("""
131 module.
132
133 enum Foo { foo1 }
134 Foo(foo1).
135 """),
136 Arguments.of("""
137 module.
138
139 class Foo.
140 Foo(Foo::new).
141 """)
142 )
143 );
144 }
145}
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
index fc51ff57..58bfce44 100644
--- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
+++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
@@ -67,8 +67,9 @@ public record WrappedProblem(Problem problem) {
67 return ProblemNavigationUtil.named(problem.getNodes(), name); 67 return ProblemNavigationUtil.named(problem.getNodes(), name);
68 } 68 }
69 69
70 public Node individualNode(String name) { 70 public Node atomNode(String name) {
71 var uniqueNodes = statementsOfType(IndividualDeclaration.class) 71 var uniqueNodes = statementsOfType(NodeDeclaration.class)
72 .filter(declaration -> declaration.getKind() == NodeKind.ATOM)
72 .flatMap(declaration -> declaration.getNodes().stream()); 73 .flatMap(declaration -> declaration.getNodes().stream());
73 return ProblemNavigationUtil.named(uniqueNodes, name); 74 return ProblemNavigationUtil.named(uniqueNodes, name);
74 } 75 }