diff options
author | Kristóf Marussy <kristof@marussy.com> | 2023-12-24 19:42:21 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2023-12-25 00:04:37 +0100 |
commit | 1f9b4652b1f85fc9f2cefcd46b34431ecea5c381 (patch) | |
tree | 84aba7f8ce90ff5aea38c912b3999ec7810f6f44 | |
parent | feat: command line model generator (diff) | |
download | refinery-1f9b4652b1f85fc9f2cefcd46b34431ecea5c381.tar.gz refinery-1f9b4652b1f85fc9f2cefcd46b34431ecea5c381.tar.zst refinery-1f9b4652b1f85fc9f2cefcd46b34431ecea5c381.zip |
refactor(generator): scope overrides
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; | |||
11 | import org.eclipse.emf.ecore.resource.Resource; | 11 | import org.eclipse.emf.ecore.resource.Resource; |
12 | import tools.refinery.generator.ModelGeneratorFactory; | 12 | import tools.refinery.generator.ModelGeneratorFactory; |
13 | import tools.refinery.generator.ProblemLoader; | 13 | import tools.refinery.generator.ProblemLoader; |
14 | import tools.refinery.language.model.problem.Problem; | ||
15 | import tools.refinery.language.model.problem.Relation; | ||
16 | import tools.refinery.language.model.problem.ScopeDeclaration; | ||
17 | 14 | ||
18 | import java.io.ByteArrayOutputStream; | ||
19 | import java.io.FileOutputStream; | 15 | import java.io.FileOutputStream; |
20 | import java.io.IOException; | 16 | import java.io.IOException; |
21 | import java.nio.charset.StandardCharsets; | ||
22 | import java.util.ArrayList; | 17 | import java.util.ArrayList; |
23 | import java.util.HashSet; | ||
24 | import java.util.List; | 18 | import java.util.List; |
25 | import java.util.Map; | 19 | import 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; | |||
17 | import org.eclipse.xtext.validation.CheckMode; | 17 | import org.eclipse.xtext.validation.CheckMode; |
18 | import org.eclipse.xtext.validation.IResourceValidator; | 18 | import org.eclipse.xtext.validation.IResourceValidator; |
19 | import tools.refinery.language.model.problem.Problem; | 19 | import tools.refinery.language.model.problem.Problem; |
20 | import tools.refinery.language.model.problem.Relation; | ||
21 | import tools.refinery.language.model.problem.ScopeDeclaration; | ||
20 | import tools.refinery.store.util.CancellationToken; | 22 | import tools.refinery.store.util.CancellationToken; |
21 | 23 | ||
24 | import java.io.ByteArrayOutputStream; | ||
22 | import java.io.File; | 25 | import java.io.File; |
23 | import java.io.IOException; | 26 | import java.io.IOException; |
24 | import java.io.InputStream; | 27 | import java.io.InputStream; |
28 | import java.nio.charset.StandardCharsets; | ||
29 | import java.util.ArrayList; | ||
30 | import java.util.HashSet; | ||
31 | import java.util.List; | ||
25 | import java.util.Map; | 32 | import java.util.Map; |
26 | 33 | ||
27 | public class ProblemLoader { | 34 | public 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 | */ | ||
6 | package tools.refinery.generator; | ||
7 | |||
8 | |||
9 | import com.google.inject.Inject; | ||
10 | import org.eclipse.xtext.testing.InjectWith; | ||
11 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
12 | import org.junit.jupiter.api.extension.ExtendWith; | ||
13 | import org.junit.jupiter.params.ParameterizedTest; | ||
14 | import org.junit.jupiter.params.provider.Arguments; | ||
15 | import org.junit.jupiter.params.provider.MethodSource; | ||
16 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
17 | |||
18 | import java.io.ByteArrayOutputStream; | ||
19 | import java.io.IOException; | ||
20 | import java.nio.charset.StandardCharsets; | ||
21 | import java.util.List; | ||
22 | import java.util.Map; | ||
23 | import java.util.stream.Stream; | ||
24 | |||
25 | import static org.hamcrest.MatcherAssert.assertThat; | ||
26 | import static org.hamcrest.Matchers.is; | ||
27 | |||
28 | @ExtendWith(InjectionExtension.class) | ||
29 | @InjectWith(ProblemInjectorProvider.class) | ||
30 | class 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 | } | ||