aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-12-24 19:42:21 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-12-25 00:04:37 +0100
commit1f9b4652b1f85fc9f2cefcd46b34431ecea5c381 (patch)
tree84aba7f8ce90ff5aea38c912b3999ec7810f6f44
parentfeat: command line model generator (diff)
downloadrefinery-1f9b4652b1f85fc9f2cefcd46b34431ecea5c381.tar.gz
refinery-1f9b4652b1f85fc9f2cefcd46b34431ecea5c381.tar.zst
refinery-1f9b4652b1f85fc9f2cefcd46b34431ecea5c381.zip
refactor(generator): scope overrides
-rw-r--r--subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java72
-rw-r--r--subprojects/generator/build.gradle.kts1
-rw-r--r--subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java61
-rw-r--r--subprojects/generator/src/test/java/tools/refinery/generator/ProblemLoaderTest.java129
4 files changed, 200 insertions, 63 deletions
diff --git a/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java
index 6c1d105d..b33fce23 100644
--- a/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java
+++ b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java
@@ -11,16 +11,10 @@ import com.google.inject.Inject;
11import org.eclipse.emf.ecore.resource.Resource; 11import org.eclipse.emf.ecore.resource.Resource;
12import tools.refinery.generator.ModelGeneratorFactory; 12import tools.refinery.generator.ModelGeneratorFactory;
13import tools.refinery.generator.ProblemLoader; 13import tools.refinery.generator.ProblemLoader;
14import tools.refinery.language.model.problem.Problem;
15import tools.refinery.language.model.problem.Relation;
16import tools.refinery.language.model.problem.ScopeDeclaration;
17 14
18import java.io.ByteArrayOutputStream;
19import java.io.FileOutputStream; 15import java.io.FileOutputStream;
20import java.io.IOException; 16import java.io.IOException;
21import java.nio.charset.StandardCharsets;
22import java.util.ArrayList; 17import java.util.ArrayList;
23import java.util.HashSet;
24import java.util.List; 18import java.util.List;
25import java.util.Map; 19import java.util.Map;
26 20
@@ -32,15 +26,15 @@ public class GenerateCommand {
32 @Inject 26 @Inject
33 private ModelGeneratorFactory generatorFactory; 27 private ModelGeneratorFactory generatorFactory;
34 28
35 private String problemPath; 29 private String inputPath;
36 private String outputPath = "-"; 30 private String outputPath = "-";
37 private List<String> scopes = new ArrayList<>(); 31 private List<String> scopes = new ArrayList<>();
38 private List<String> overrideScopes = new ArrayList<>(); 32 private List<String> overrideScopes = new ArrayList<>();
39 private long randomSeed = 1; 33 private long randomSeed = 1;
40 34
41 @Parameter(description = "Input path", required = true) 35 @Parameter(description = "input path", required = true)
42 public void setProblemPath(String problemPath) { 36 public void setInputPath(String inputPath) {
43 this.problemPath = problemPath; 37 this.inputPath = inputPath;
44 } 38 }
45 39
46 @Parameter(names = {"-output", "-o"}, description = "Output path") 40 @Parameter(names = {"-output", "-o"}, description = "Output path")
@@ -64,14 +58,15 @@ public class GenerateCommand {
64 } 58 }
65 59
66 public void run() throws IOException { 60 public void run() throws IOException {
67 var problem = addScopeConstraints(loader.loadFile(problemPath)); 61 var problem = isStandardStream(inputPath) ? loader.loadStream(System.in) : loader.loadFile(inputPath);
62 problem = loader.loadScopeConstraints(problem, scopes, overrideScopes);
68 var generator = generatorFactory.createGenerator(problem); 63 var generator = generatorFactory.createGenerator(problem);
69 generator.setRandomSeed(randomSeed); 64 generator.setRandomSeed(randomSeed);
70 generator.generate(); 65 generator.generate();
71 var solution = generator.serializeSolution(); 66 var solution = generator.serializeSolution();
72 var solutionResource = solution.eResource(); 67 var solutionResource = solution.eResource();
73 var saveOptions = Map.of(); 68 var saveOptions = Map.of();
74 if (outputPath == null || outputPath.equals("-")) { 69 if (isStandardStream(outputPath)) {
75 printSolution(solutionResource, saveOptions); 70 printSolution(solutionResource, saveOptions);
76 } else { 71 } else {
77 try (var outputStream = new FileOutputStream(outputPath)) { 72 try (var outputStream = new FileOutputStream(outputPath)) {
@@ -80,57 +75,8 @@ public class GenerateCommand {
80 } 75 }
81 } 76 }
82 77
83 private Problem addScopeConstraints(Problem problem) throws IOException { 78 private boolean isStandardStream(String path) {
84 var allScopes = new ArrayList<>(scopes); 79 return path == null || path.equals("-");
85 allScopes.addAll(overrideScopes);
86 if (allScopes.isEmpty()) {
87 return problem;
88 }
89 int originalStatementCount = problem.getStatements().size();
90 var builder = new StringBuilder();
91 var problemResource = problem.eResource();
92 try (var outputStream = new ByteArrayOutputStream()) {
93 problemResource.save(outputStream, Map.of());
94 builder.append(outputStream.toString(StandardCharsets.UTF_8));
95 }
96 builder.append('\n');
97 for (var scope : allScopes) {
98 builder.append("scope ").append(scope).append(".\n");
99 }
100 var modifiedProblem = loader.loadString(builder.toString(), problemResource.getURI());
101 var modifiedStatements = modifiedProblem.getStatements();
102 int modifiedStatementCount = modifiedStatements.size();
103 if (modifiedStatementCount != originalStatementCount + allScopes.size()) {
104 throw new IllegalStateException("Failed to parse scope constraints");
105 }
106 // Override scopes remove any scope constraint from the original problem with the same target type.
107 var overriddenScopes = new HashSet<Relation>();
108 for (int i = modifiedStatementCount - overrideScopes.size(); i < modifiedStatementCount; i++) {
109 var statement = modifiedStatements.get(i);
110 if (!(statement instanceof ScopeDeclaration scopeDeclaration)) {
111 throw new IllegalStateException("Invalid scope constraint: " + statement);
112 }
113 for (var typeScope : scopeDeclaration.getTypeScopes()) {
114 overriddenScopes.add(typeScope.getTargetType());
115 }
116 }
117 int statementIndex = 0;
118 var iterator = modifiedStatements.iterator();
119 // Scope overrides only affect type scopes from the original problem and leave type scopes added on the
120 // command line intact.
121 while (statementIndex < originalStatementCount && iterator.hasNext()) {
122 var statement = iterator.next();
123 if (statement instanceof ScopeDeclaration scopeDeclaration) {
124 var typeScopes = scopeDeclaration.getTypeScopes();
125 typeScopes.removeIf(typeScope -> overriddenScopes.contains(typeScope.getTargetType()));
126 // Scope declarations with no type scopes are invalid, so we have to remove them.
127 if (typeScopes.isEmpty()) {
128 iterator.remove();
129 }
130 }
131 statementIndex++;
132 }
133 return modifiedProblem;
134 } 80 }
135 81
136 // We deliberately write to the standard output if no output path is specified. 82 // We deliberately write to the standard output if no output path is specified.
diff --git a/subprojects/generator/build.gradle.kts b/subprojects/generator/build.gradle.kts
index d87ce6de..f78fee4d 100644
--- a/subprojects/generator/build.gradle.kts
+++ b/subprojects/generator/build.gradle.kts
@@ -12,4 +12,5 @@ dependencies {
12 api(project(":refinery-language-semantics")) 12 api(project(":refinery-language-semantics"))
13 api(libs.eclipseCollections.api) 13 api(libs.eclipseCollections.api)
14 implementation(project(":refinery-store-query-interpreter")) 14 implementation(project(":refinery-store-query-interpreter"))
15 testImplementation(testFixtures(project(":refinery-language")))
15} 16}
diff --git a/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java b/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java
index abfbe7e6..20ea8132 100644
--- a/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java
+++ b/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java
@@ -17,11 +17,18 @@ import org.eclipse.xtext.util.LazyStringInputStream;
17import org.eclipse.xtext.validation.CheckMode; 17import org.eclipse.xtext.validation.CheckMode;
18import org.eclipse.xtext.validation.IResourceValidator; 18import org.eclipse.xtext.validation.IResourceValidator;
19import tools.refinery.language.model.problem.Problem; 19import tools.refinery.language.model.problem.Problem;
20import tools.refinery.language.model.problem.Relation;
21import tools.refinery.language.model.problem.ScopeDeclaration;
20import tools.refinery.store.util.CancellationToken; 22import tools.refinery.store.util.CancellationToken;
21 23
24import java.io.ByteArrayOutputStream;
22import java.io.File; 25import java.io.File;
23import java.io.IOException; 26import java.io.IOException;
24import java.io.InputStream; 27import java.io.InputStream;
28import java.nio.charset.StandardCharsets;
29import java.util.ArrayList;
30import java.util.HashSet;
31import java.util.List;
25import java.util.Map; 32import java.util.Map;
26 33
27public class ProblemLoader { 34public class ProblemLoader {
@@ -104,4 +111,58 @@ public class ProblemLoader {
104 } 111 }
105 return problem; 112 return problem;
106 } 113 }
114
115 public Problem loadScopeConstraints(Problem problem, List<String> extraScopes,
116 List<String> overrideScopes) throws IOException {
117 var allScopes = new ArrayList<>(extraScopes);
118 allScopes.addAll(overrideScopes);
119 if (allScopes.isEmpty()) {
120 return problem;
121 }
122 int originalStatementCount = problem.getStatements().size();
123 var builder = new StringBuilder();
124 var problemResource = problem.eResource();
125 try (var outputStream = new ByteArrayOutputStream()) {
126 problemResource.save(outputStream, Map.of());
127 builder.append(outputStream.toString(StandardCharsets.UTF_8));
128 }
129 builder.append('\n');
130 for (var scope : allScopes) {
131 builder.append("scope ").append(scope).append(".\n");
132 }
133 var modifiedProblem = loadString(builder.toString(), problemResource.getURI());
134 var modifiedStatements = modifiedProblem.getStatements();
135 int modifiedStatementCount = modifiedStatements.size();
136 if (modifiedStatementCount != originalStatementCount + allScopes.size()) {
137 throw new IllegalArgumentException("Failed to parse scope constraints");
138 }
139 // Override scopes remove any scope constraint from the original problem with the same target type.
140 var overriddenScopes = new HashSet<Relation>();
141 for (int i = modifiedStatementCount - overrideScopes.size(); i < modifiedStatementCount; i++) {
142 var statement = modifiedStatements.get(i);
143 if (!(statement instanceof ScopeDeclaration scopeDeclaration)) {
144 throw new IllegalStateException("Invalid scope constraint: " + statement);
145 }
146 for (var typeScope : scopeDeclaration.getTypeScopes()) {
147 overriddenScopes.add(typeScope.getTargetType());
148 }
149 }
150 int statementIndex = 0;
151 var iterator = modifiedStatements.iterator();
152 // Scope overrides only affect type scopes from the original problem and leave type scopes added on the
153 // command line intact.
154 while (statementIndex < originalStatementCount && iterator.hasNext()) {
155 var statement = iterator.next();
156 if (statement instanceof ScopeDeclaration scopeDeclaration) {
157 var typeScopes = scopeDeclaration.getTypeScopes();
158 typeScopes.removeIf(typeScope -> overriddenScopes.contains(typeScope.getTargetType()));
159 // Scope declarations with no type scopes are invalid, so we have to remove them.
160 if (typeScopes.isEmpty()) {
161 iterator.remove();
162 }
163 }
164 statementIndex++;
165 }
166 return modifiedProblem;
167 }
107} 168}
diff --git a/subprojects/generator/src/test/java/tools/refinery/generator/ProblemLoaderTest.java b/subprojects/generator/src/test/java/tools/refinery/generator/ProblemLoaderTest.java
new file mode 100644
index 00000000..0c0db105
--- /dev/null
+++ b/subprojects/generator/src/test/java/tools/refinery/generator/ProblemLoaderTest.java
@@ -0,0 +1,129 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator;
7
8
9import com.google.inject.Inject;
10import org.eclipse.xtext.testing.InjectWith;
11import org.eclipse.xtext.testing.extensions.InjectionExtension;
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.tests.ProblemInjectorProvider;
17
18import java.io.ByteArrayOutputStream;
19import java.io.IOException;
20import java.nio.charset.StandardCharsets;
21import java.util.List;
22import java.util.Map;
23import java.util.stream.Stream;
24
25import static org.hamcrest.MatcherAssert.assertThat;
26import static org.hamcrest.Matchers.is;
27
28@ExtendWith(InjectionExtension.class)
29@InjectWith(ProblemInjectorProvider.class)
30class ProblemLoaderTest {
31 private static final String PREFIX = """
32 class Foo.
33 class Bar.
34 """;
35
36 @Inject
37 private ProblemLoader loader;
38
39 @ParameterizedTest
40 @MethodSource
41 void loadScopeConstraintsTest(String originalScopes, List<String> scopes, List<String> overrideScopes,
42 String expectedScopes) throws IOException {
43 var problem = loader.loadString(PREFIX + originalScopes);
44 var modifiedProblem = loader.loadScopeConstraints(problem, scopes, overrideScopes);
45 String serializedProblem;
46 try (var outputStream = new ByteArrayOutputStream()) {
47 modifiedProblem.eResource().save(outputStream, Map.of());
48 serializedProblem = outputStream.toString(StandardCharsets.UTF_8);
49 }
50 assertThat(serializedProblem, is(PREFIX + expectedScopes));
51 }
52
53 static Stream<Arguments> loadScopeConstraintsTest() {
54 return Stream.of(Arguments.of("",
55 List.of(),
56 List.of(),
57 ""), Arguments.of("",
58 List.of("node=5..10"),
59 List.of(), """
60
61 scope node=5..10.
62 """), Arguments.of("",
63 List.of("Foo=2", "Bar=3"),
64 List.of(), """
65
66 scope Foo=2.
67 scope Bar=3.
68 """), Arguments.of("""
69 scope Foo = 1, Bar = 1.
70 """,
71 List.of("node=5..10"),
72 List.of(), """
73 scope Foo = 1, Bar = 1.
74
75 scope node=5..10.
76 """), Arguments.of("""
77 scope Foo = 0..10, Bar = 1.
78 """,
79 List.of("Foo = 5"),
80 List.of(), """
81 scope Foo = 0..10, Bar = 1.
82
83 scope Foo = 5.
84 """), Arguments.of("""
85 scope Foo = 1, Bar = 1.
86 """,
87 List.of(),
88 List.of("node=5..10"), """
89 scope Foo = 1, Bar = 1.
90
91 scope node=5..10.
92 """), Arguments.of("""
93 scope Foo = 1, Bar = 1.
94 """,
95 List.of(),
96 List.of("Foo=3..4"), """
97 scope Bar = 1.
98
99 scope Foo=3..4.
100 """), Arguments.of("""
101 scope Foo = 1, Bar = 1.
102 """,
103 List.of("Foo=2"),
104 List.of("Foo=3..4"), """
105 scope Bar = 1.
106
107 scope Foo=2.
108 scope Foo=3..4.
109 """), Arguments.of("""
110 scope Foo = 1.
111 scope Bar = 1.
112 """,
113 List.of(),
114 List.of("Bar=3..4"), """
115 scope Foo = 1.
116
117
118 scope Bar=3..4.
119 """), Arguments.of("""
120 scope Foo = 1, Bar = 1.
121 """,
122 List.of(),
123 List.of("Foo=3..4", "Bar=4..5"), """
124
125 scope Foo=3..4.
126 scope Bar=4..5.
127 """));
128 }
129}