diff options
Diffstat (limited to 'subprojects')
4 files changed, 338 insertions, 220 deletions
diff --git a/subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java b/subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java index ed551b6e..ad203316 100644 --- a/subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java +++ b/subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java | |||
@@ -1,5 +1,6 @@ | |||
1 | /******************************************************************************* | 1 | /******************************************************************************* |
2 | * Copyright (c) 2010-2016, Gabor Bergmann, Istvan Rath and Daniel Varro | 2 | * Copyright (c) 2010-2016, Gabor Bergmann, Istvan Rath and Daniel Varro |
3 | * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * This program and the accompanying materials are made available under the | 4 | * This program and the accompanying materials are made available under the |
4 | * terms of the Eclipse Public License v. 2.0 which is available at | 5 | * terms of the Eclipse Public License v. 2.0 which is available at |
5 | * http://www.eclipse.org/legal/epl-v20.html. | 6 | * http://www.eclipse.org/legal/epl-v20.html. |
@@ -9,16 +10,15 @@ | |||
9 | package tools.refinery.interpreter.rete.recipes.helper; | 10 | package tools.refinery.interpreter.rete.recipes.helper; |
10 | 11 | ||
11 | import org.eclipse.emf.common.util.EList; | 12 | import org.eclipse.emf.common.util.EList; |
12 | import org.eclipse.emf.ecore.EAttribute; | 13 | import org.eclipse.emf.ecore.*; |
13 | import org.eclipse.emf.ecore.EClass; | ||
14 | import org.eclipse.emf.ecore.EObject; | ||
15 | import org.eclipse.emf.ecore.EStructuralFeature; | ||
16 | import org.eclipse.emf.ecore.util.EcoreUtil; | 14 | import org.eclipse.emf.ecore.util.EcoreUtil; |
15 | import org.jetbrains.annotations.Nullable; | ||
17 | import tools.refinery.interpreter.rete.recipes.RecipesPackage; | 16 | import tools.refinery.interpreter.rete.recipes.RecipesPackage; |
18 | import tools.refinery.interpreter.rete.recipes.ReteNodeRecipe; | 17 | import tools.refinery.interpreter.rete.recipes.ReteNodeRecipe; |
19 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext; | 18 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext; |
20 | 19 | ||
21 | import java.util.*; | 20 | import java.util.*; |
21 | import java.util.concurrent.atomic.AtomicLong; | ||
22 | 22 | ||
23 | /** | 23 | /** |
24 | * Stores a set of known <em>canonical</em> recipes, each representing a disjoint equivalence class of recipes, modulo | 24 | * Stores a set of known <em>canonical</em> recipes, each representing a disjoint equivalence class of recipes, modulo |
@@ -26,179 +26,312 @@ import java.util.*; | |||
26 | * | 26 | * |
27 | * @author Gabor Bergmann | 27 | * @author Gabor Bergmann |
28 | * @since 1.3 | 28 | * @since 1.3 |
29 | * | ||
30 | */ | 29 | */ |
31 | public class RecipeRecognizer { | 30 | public class RecipeRecognizer { |
32 | private static long nextRecipeEquivalenceClassID = 0; | 31 | private static final AtomicLong nextRecipeEquivalenceClassID = new AtomicLong(0); |
33 | 32 | ||
34 | /** | 33 | /** |
35 | * if EcoreUtil.equals(recipe1, recipe2), only one of them will be included here | 34 | * if EcoreUtil.equals(recipe1, recipe2), only one of them will be included here |
36 | */ | 35 | */ |
37 | Map<EClass, Set<ReteNodeRecipe>> canonicalRecipesByClass = new HashMap<>(); | 36 | Map<Long, Set<ReteNodeRecipe>> canonicalRecipesByHashCode = new HashMap<>(); |
38 | Map<Long, ReteNodeRecipe> canonicalRecipeByEquivalenceClassID = new HashMap<>(); | 37 | Map<Long, ReteNodeRecipe> canonicalRecipeByEquivalenceClassID = new HashMap<>(); |
39 | 38 | Set<ReteNodeRecipe> canonicalRecipesWithoutHashCode = new LinkedHashSet<>(); | |
40 | private IQueryRuntimeContext runtimeContext; | 39 | |
41 | 40 | private final IQueryRuntimeContext runtimeContext; | |
42 | /** | 41 | |
43 | * @param can be null; if provided, further equivalences can be detected based on {@link IQueryRuntimeContext#wrapElement(Object)} | 42 | /** |
44 | * @since 1.6 | 43 | * @param runtimeContext can be null; if provided, further equivalences can be detected based on |
45 | */ | 44 | * {@link IQueryRuntimeContext#wrapElement(Object)} |
46 | public RecipeRecognizer(IQueryRuntimeContext runtimeContext) { | 45 | * @since 1.6 |
47 | this.runtimeContext = runtimeContext; | 46 | */ |
48 | } | 47 | public RecipeRecognizer(IQueryRuntimeContext runtimeContext) { |
49 | public RecipeRecognizer() { | 48 | this.runtimeContext = runtimeContext; |
50 | this(null); | 49 | } |
51 | } | 50 | |
52 | 51 | public RecipeRecognizer() { | |
53 | /** | 52 | this(null); |
54 | * Recognizes when an equivalent canonical recipe is already known. | 53 | } |
55 | * | 54 | |
56 | * @return an equivalent canonical recipe, or the null if no known equivalent found | 55 | /** |
57 | */ | 56 | * Recognizes when an equivalent canonical recipe is already known. |
58 | public ReteNodeRecipe peekCanonicalRecipe(final ReteNodeRecipe recipe) { | 57 | * |
59 | // equivalence class already known | 58 | * @return an equivalent canonical recipe, or the null if no known equivalent found |
60 | for (Long classID : recipe.getEquivalenceClassIDs()) { | 59 | */ |
61 | ReteNodeRecipe knownRecipe = canonicalRecipeByEquivalenceClassID.get(classID); | 60 | public ReteNodeRecipe peekCanonicalRecipe(final ReteNodeRecipe recipe) { |
62 | if (knownRecipe != null) | 61 | var recipeByEquivalenceClass = getRecipeByEquivalenceClass(recipe); |
63 | return knownRecipe; | 62 | if (recipeByEquivalenceClass != null) { |
64 | } | 63 | return recipeByEquivalenceClass; |
65 | 64 | } | |
66 | // equivalence class not known, but maybe equivalent recipe still | 65 | |
67 | // available | 66 | var hashCode = computeHashCode(recipe); |
68 | Collection<ReteNodeRecipe> sameClassRecipes = getSameClassCanonicalRecipes(recipe); | 67 | if (hashCode != null) { |
69 | for (ReteNodeRecipe knownRecipe : sameClassRecipes) { | 68 | var recipeByHashCode = getRecipeByHashCode(recipe, hashCode); |
70 | if (isEquivalentRecipe(recipe, knownRecipe)) { | 69 | if (recipeByHashCode != null) { |
71 | // FOUND EQUIVALENT RECIPE | 70 | return recipeByHashCode; |
72 | recipe.getEquivalenceClassIDs().add(knownRecipe.getEquivalenceClassIDs().get(0)); | 71 | } |
73 | return knownRecipe; | 72 | } |
74 | } | 73 | |
75 | } | 74 | // If we already designated {@code recipe} as canonical during a recursive call in {@code computeHashCode}, |
76 | 75 | // it will be found here, and we will move it to {@code canonicalRecipesByHashCode}. This could be improved by | |
77 | return null; | 76 | // checking whether {@code recipe} is already canonical explicitly if there are many recursive patterns. |
78 | } | 77 | return getRecipeAndAssignHashCode(recipe, hashCode); |
79 | 78 | } | |
80 | /** | 79 | |
81 | * This recipe will be remembered as a canonical recipe. Method maintains both internal data structures and the | 80 | @Nullable |
82 | * equivalence class attribute of the recipe. PRECONDITION: {@link #peekCanonicalRecipe(ReteNodeRecipe)} must return | 81 | private ReteNodeRecipe getRecipeByEquivalenceClass(ReteNodeRecipe recipe) { |
83 | * null or the recipe itself | 82 | for (Long classID : recipe.getEquivalenceClassIDs()) { |
84 | */ | 83 | ReteNodeRecipe knownRecipe = canonicalRecipeByEquivalenceClassID.get(classID); |
85 | public void makeCanonical(final ReteNodeRecipe recipe) { | 84 | if (knownRecipe != null) { |
86 | // this is a canonical recipe, chosen representative of its new | 85 | return knownRecipe; |
87 | // equivalence class | 86 | } |
88 | if (recipe.getEquivalenceClassIDs().isEmpty()) { | 87 | } |
89 | recipe.getEquivalenceClassIDs().add(nextRecipeEquivalenceClassID++); | 88 | return null; |
90 | } | 89 | } |
91 | for (Long classID : recipe.getEquivalenceClassIDs()) { | 90 | |
92 | canonicalRecipeByEquivalenceClassID.put(classID, recipe); | 91 | @Nullable |
93 | } | 92 | private ReteNodeRecipe getRecipeByHashCode(ReteNodeRecipe recipe, Long hashCode) { |
94 | getSameClassCanonicalRecipes(recipe).add(recipe); | 93 | var equivalentRecipesByHashCode = canonicalRecipesByHashCode.get(hashCode); |
95 | } | 94 | if (equivalentRecipesByHashCode != null) { |
96 | 95 | for (ReteNodeRecipe knownRecipe : equivalentRecipesByHashCode) { | |
97 | /** | 96 | if (isEquivalentRecipe(recipe, knownRecipe)) { |
98 | * Ensures that there is an equivalent canonical recipe; if none is known yet, this recipe will be remembered as | 97 | // FOUND EQUIVALENT RECIPE |
99 | * canonical. | 98 | recipe.getEquivalenceClassIDs().add(knownRecipe.getEquivalenceClassIDs().get(0)); |
100 | * | 99 | return knownRecipe; |
101 | * @return an equivalent canonical recipe; the argument recipe itself (which is made canonical) if no known | 100 | } |
102 | * equivalent found | 101 | } |
103 | */ | 102 | } |
104 | public ReteNodeRecipe canonicalizeRecipe(final ReteNodeRecipe recipe) { | 103 | return null; |
105 | ReteNodeRecipe knownRecipe = peekCanonicalRecipe(recipe); | 104 | } |
106 | if (knownRecipe == null) { | 105 | |
107 | knownRecipe = recipe; | 106 | @Nullable |
108 | makeCanonical(recipe); | 107 | private ReteNodeRecipe getRecipeAndAssignHashCode(ReteNodeRecipe recipe, Long hashCode) { |
109 | } | 108 | var iterator = canonicalRecipesWithoutHashCode.iterator(); |
110 | return knownRecipe; | 109 | while (iterator.hasNext()) { |
111 | } | 110 | var knownRecipe = iterator.next(); |
112 | 111 | if (isEquivalentRecipe(recipe, knownRecipe)) { | |
113 | /** | 112 | // FOUND EQUIVALENT RECIPE |
114 | * @return true iff recipe is a canonical recipe | 113 | recipe.getEquivalenceClassIDs().add(knownRecipe.getEquivalenceClassIDs().get(0)); |
115 | */ | 114 | var cachedHashCode = knownRecipe.getCachedHashCode(); |
116 | public boolean isKnownCanonicalRecipe(final ReteNodeRecipe recipe) { | 115 | if (cachedHashCode != null && !cachedHashCode.equals(hashCode)) { |
117 | if (recipe.getEquivalenceClassIDs().isEmpty()) { | 116 | throw new AssertionError("Cached recipe %s already had hash code %s" |
118 | return false; | 117 | .formatted(knownRecipe, cachedHashCode)); |
119 | } | 118 | } |
120 | ReteNodeRecipe knownRecipe = canonicalRecipeByEquivalenceClassID.get(recipe.getEquivalenceClassIDs().get(0)); | 119 | if (hashCode != null) { |
121 | return recipe == knownRecipe; | 120 | knownRecipe.setCachedHashCode(hashCode); |
122 | } | 121 | addHashCodeRepresentative(hashCode, knownRecipe); |
123 | 122 | iterator.remove(); | |
124 | private Set<ReteNodeRecipe> getSameClassCanonicalRecipes(final ReteNodeRecipe recipe) { | 123 | } |
125 | Set<ReteNodeRecipe> sameClassRecipes = canonicalRecipesByClass.get(recipe.eClass()); | 124 | return knownRecipe; |
126 | if (sameClassRecipes == null) { | 125 | } |
127 | sameClassRecipes = new HashSet<>(); | 126 | } |
128 | canonicalRecipesByClass.put(recipe.eClass(), sameClassRecipes); | 127 | return null; |
129 | } | 128 | } |
130 | return sameClassRecipes; | 129 | |
131 | } | 130 | private final Deque<EObject> hashCodeStack = new ArrayDeque<>(); |
132 | 131 | ||
133 | private boolean isEquivalentRecipe(ReteNodeRecipe recipe, ReteNodeRecipe knownRecipe) { | 132 | private Long computeHashCode(Object object) { |
134 | return new EqualityHelper(runtimeContext).equals(recipe, knownRecipe); | 133 | if (object instanceof List<?> list) { |
135 | } | 134 | return computeListHashCode(list); |
136 | 135 | } | |
137 | // TODO reuse in more cases later, e.g. switching join node parents, etc. | 136 | if (object instanceof ReteNodeRecipe recipe) { |
138 | private static class EqualityHelper extends EcoreUtil.EqualityHelper { | 137 | return ensureRecipeHashCode(recipe); |
139 | 138 | } | |
140 | 139 | if (object instanceof EObject eObject) { | |
141 | 140 | return computeEObjectHashCode(eObject); | |
142 | 141 | } | |
143 | private static final long serialVersionUID = -8841971394686015188L; | 142 | return (long) Objects.hashCode(object); |
144 | 143 | } | |
145 | private static final EAttribute RETE_NODE_RECIPE_EQUIVALENCE_CLASS_IDS = | 144 | |
146 | RecipesPackage.eINSTANCE.getReteNodeRecipe_EquivalenceClassIDs(); | 145 | private Long computeHashCodeOrEquivalenceClassId(Object object) { |
147 | private static final EAttribute CONSTANT_RECIPE_CONSTANT_VALUES = | 146 | if (object instanceof ReteNodeRecipe recipe) { |
148 | RecipesPackage.eINSTANCE.getConstantRecipe_ConstantValues(); | 147 | var equivalenceClassIDs = recipe.getEquivalenceClassIDs(); |
149 | private static final EAttribute DISCRIMINATOR_BUCKET_RECIPE_BUCKET_KEY = | 148 | if (!equivalenceClassIDs.isEmpty()) { |
150 | RecipesPackage.eINSTANCE.getDiscriminatorBucketRecipe_BucketKey(); | 149 | return equivalenceClassIDs.get(0); |
151 | 150 | } | |
152 | private IQueryRuntimeContext runtimeContext; | 151 | if (hashCodeStack.contains(recipe)) { |
153 | 152 | return null; | |
154 | public EqualityHelper(IQueryRuntimeContext runtimeContext) { | 153 | } |
155 | this.runtimeContext = runtimeContext; | 154 | var canonicalRecipe = canonicalizeRecipe(recipe); |
156 | } | 155 | return canonicalRecipe.getEquivalenceClassIDs().get(0); |
157 | 156 | } else { | |
158 | @Override | 157 | return computeHashCode(object); |
159 | protected boolean haveEqualFeature(EObject eObject1, EObject eObject2, EStructuralFeature feature) { | 158 | } |
160 | // ignore differences in this attribute, as it may only be assigned | 159 | } |
161 | // after the equivalence check | 160 | |
162 | if (RETE_NODE_RECIPE_EQUIVALENCE_CLASS_IDS.equals(feature)) | 161 | private Long computeListHashCode(List<?> list) { |
163 | return true; | 162 | long result = 1; |
164 | 163 | for (var item : list) { | |
165 | if (runtimeContext != null) { | 164 | var update = computeHashCodeOrEquivalenceClassId(item); |
166 | // constant values | 165 | if (update == null) { |
167 | if (/*CONSTANT_RECIPE_CONSTANT_VALUES.equals(feature) ||*/ DISCRIMINATOR_BUCKET_RECIPE_BUCKET_KEY.equals(feature)) { | 166 | return null; |
168 | // use runtime context to map to canonical wrapped form | 167 | } |
169 | // this is costly for constant recipes (TODO improve this), but essential for discriminator buckets | 168 | result = result * 37 + update; |
170 | 169 | } | |
171 | Object val1 = eObject1.eGet(feature); | 170 | return result; |
172 | Object val2 = eObject2.eGet(feature); | 171 | } |
173 | 172 | ||
174 | if (val1 != null && val2 != null) { | 173 | private Long ensureRecipeHashCode(ReteNodeRecipe recipe) { |
175 | return runtimeContext.wrapElement(val1).equals(runtimeContext.wrapElement(val2)); | 174 | var hashCode = recipe.getCachedHashCode(); |
176 | } else { | 175 | if (hashCode != null) { |
177 | return val1 == null && val2 == null; | 176 | return hashCode; |
178 | } | 177 | } |
179 | 178 | hashCode = computeEObjectHashCode(recipe); | |
180 | } | 179 | if (hashCode == null) { |
181 | } | 180 | return null; |
182 | 181 | } | |
183 | // fallback to general comparison | 182 | recipe.setCachedHashCode(hashCode); |
184 | return super.haveEqualFeature(eObject1, eObject2, feature); | 183 | return hashCode; |
185 | } | 184 | } |
186 | 185 | ||
187 | @Override | 186 | private Long computeEObjectHashCode(EObject eObject) { |
188 | public boolean equals(EObject eObject1, EObject eObject2) { | 187 | if (hashCodeStack.contains(eObject)) { |
189 | // short-circuit if already known to be equivalent | 188 | return null; |
190 | if (eObject1 instanceof ReteNodeRecipe) { | 189 | } |
191 | if (eObject2 instanceof ReteNodeRecipe) { | 190 | hashCodeStack.addLast(eObject); |
192 | EList<Long> eqClassIDs1 = ((ReteNodeRecipe) eObject1).getEquivalenceClassIDs(); | 191 | try { |
193 | EList<Long> eqClassIDs2 = ((ReteNodeRecipe) eObject2).getEquivalenceClassIDs(); | 192 | long result = eObject.eClass().hashCode(); |
194 | 193 | for (var feature : eObject.eClass().getEAllStructuralFeatures()) { | |
195 | if (!Collections.disjoint(eqClassIDs1, eqClassIDs2)) | 194 | if (eObject instanceof ReteNodeRecipe && ( |
196 | return true; | 195 | RecipesPackage.Literals.RETE_NODE_RECIPE__EQUIVALENCE_CLASS_IDS.equals(feature) || |
197 | } | 196 | RecipesPackage.Literals.RETE_NODE_RECIPE__CACHED_HASH_CODE.equals(feature) || |
198 | } | 197 | RecipesPackage.Literals.RETE_NODE_RECIPE__CONSTRUCTED.equals(feature))) { |
199 | 198 | continue; | |
200 | // fallback to general comparison | 199 | } |
201 | return super.equals(eObject1, eObject2); | 200 | var value = eObject.eGet(feature); |
202 | } | 201 | var update = computeHashCodeOrEquivalenceClassId(value); |
203 | } | 202 | if (update == null) { |
203 | return null; | ||
204 | } | ||
205 | result = result * 37 + update; | ||
206 | } | ||
207 | return result; | ||
208 | } finally { | ||
209 | hashCodeStack.removeLast(); | ||
210 | } | ||
211 | } | ||
212 | |||
213 | private void addHashCodeRepresentative(Long hashCode, ReteNodeRecipe recipe) { | ||
214 | canonicalRecipesByHashCode.computeIfAbsent(hashCode, ignored -> new LinkedHashSet<>()).add(recipe); | ||
215 | } | ||
216 | |||
217 | /** | ||
218 | * This recipe will be remembered as a canonical recipe. Method maintains both internal data structures and the | ||
219 | * equivalence class attribute of the recipe. PRECONDITION: {@link #peekCanonicalRecipe(ReteNodeRecipe)} must | ||
220 | * return null or the recipe itself | ||
221 | */ | ||
222 | public void makeCanonical(final ReteNodeRecipe recipe) { | ||
223 | // this is a canonical recipe, chosen representative of its new | ||
224 | // equivalence class | ||
225 | if (recipe.getEquivalenceClassIDs().isEmpty()) { | ||
226 | recipe.getEquivalenceClassIDs().add(nextRecipeEquivalenceClassID.getAndIncrement()); | ||
227 | } | ||
228 | for (Long classID : recipe.getEquivalenceClassIDs()) { | ||
229 | canonicalRecipeByEquivalenceClassID.put(classID, recipe); | ||
230 | } | ||
231 | var hashCode = computeHashCode(recipe); | ||
232 | if (hashCode == null) { | ||
233 | canonicalRecipesWithoutHashCode.add(recipe); | ||
234 | } else { | ||
235 | addHashCodeRepresentative(hashCode, recipe); | ||
236 | } | ||
237 | } | ||
238 | |||
239 | /** | ||
240 | * Ensures that there is an equivalent canonical recipe; if none is known yet, this recipe will be remembered as | ||
241 | * canonical. | ||
242 | * | ||
243 | * @return an equivalent canonical recipe; the argument recipe itself (which is made canonical) if no known | ||
244 | * equivalent found | ||
245 | */ | ||
246 | public ReteNodeRecipe canonicalizeRecipe(final ReteNodeRecipe recipe) { | ||
247 | ReteNodeRecipe knownRecipe = peekCanonicalRecipe(recipe); | ||
248 | if (knownRecipe == null) { | ||
249 | knownRecipe = recipe; | ||
250 | makeCanonical(recipe); | ||
251 | } | ||
252 | return knownRecipe; | ||
253 | } | ||
254 | |||
255 | /** | ||
256 | * @return true iff recipe is a canonical recipe | ||
257 | */ | ||
258 | public boolean isKnownCanonicalRecipe(final ReteNodeRecipe recipe) { | ||
259 | if (recipe.getEquivalenceClassIDs().isEmpty()) { | ||
260 | return false; | ||
261 | } | ||
262 | ReteNodeRecipe knownRecipe = canonicalRecipeByEquivalenceClassID.get(recipe.getEquivalenceClassIDs().get(0)); | ||
263 | return recipe == knownRecipe; | ||
264 | } | ||
265 | |||
266 | private boolean isEquivalentRecipe(ReteNodeRecipe recipe, ReteNodeRecipe knownRecipe) { | ||
267 | return new EqualityHelper(runtimeContext).equals(recipe, knownRecipe); | ||
268 | } | ||
269 | |||
270 | // TODO reuse in more cases later, e.g. switching join node parents, etc. | ||
271 | private static class EqualityHelper extends EcoreUtil.EqualityHelper { | ||
272 | private static final long serialVersionUID = -8841971394686015188L; | ||
273 | |||
274 | private static final EAttribute RETE_NODE_RECIPE_EQUIVALENCE_CLASS_IDS = | ||
275 | RecipesPackage.eINSTANCE.getReteNodeRecipe_EquivalenceClassIDs(); | ||
276 | private static final EAttribute RETE_NODE_RECIPE_CACHED_HASH_CODE = | ||
277 | RecipesPackage.eINSTANCE.getReteNodeRecipe_CachedHashCode(); | ||
278 | private static final EAttribute RETE_NODE_RECIPE_CONSTRUCTED = | ||
279 | RecipesPackage.eINSTANCE.getReteNodeRecipe_Constructed(); | ||
280 | private static final EAttribute DISCRIMINATOR_BUCKET_RECIPE_BUCKET_KEY = | ||
281 | RecipesPackage.eINSTANCE.getDiscriminatorBucketRecipe_BucketKey(); | ||
282 | |||
283 | private final IQueryRuntimeContext runtimeContext; | ||
284 | |||
285 | public EqualityHelper(IQueryRuntimeContext runtimeContext) { | ||
286 | this.runtimeContext = runtimeContext; | ||
287 | } | ||
288 | |||
289 | @Override | ||
290 | protected boolean haveEqualFeature(EObject eObject1, EObject eObject2, EStructuralFeature feature) { | ||
291 | // ignore differences in this attribute, as it may only be assigned | ||
292 | // after the equivalence check | ||
293 | if (RETE_NODE_RECIPE_EQUIVALENCE_CLASS_IDS.equals(feature) || | ||
294 | RETE_NODE_RECIPE_CACHED_HASH_CODE.equals(feature) || | ||
295 | RETE_NODE_RECIPE_CONSTRUCTED.equals(feature)) | ||
296 | return true; | ||
297 | |||
298 | if (runtimeContext != null) { | ||
299 | // constant values | ||
300 | if (DISCRIMINATOR_BUCKET_RECIPE_BUCKET_KEY.equals(feature)) { | ||
301 | // use runtime context to map to canonical wrapped form | ||
302 | // this is costly for constant recipes (TODO improve this), but essential for discriminator buckets | ||
303 | |||
304 | Object val1 = eObject1.eGet(feature); | ||
305 | Object val2 = eObject2.eGet(feature); | ||
306 | |||
307 | if (val1 != null && val2 != null) { | ||
308 | return runtimeContext.wrapElement(val1).equals(runtimeContext.wrapElement(val2)); | ||
309 | } else { | ||
310 | return val1 == null && val2 == null; | ||
311 | } | ||
312 | |||
313 | } | ||
314 | } | ||
315 | |||
316 | // fallback to general comparison | ||
317 | return super.haveEqualFeature(eObject1, eObject2, feature); | ||
318 | } | ||
319 | |||
320 | @Override | ||
321 | public boolean equals(EObject eObject1, EObject eObject2) { | ||
322 | // short-circuit if already known to be equivalent | ||
323 | if (eObject1 instanceof ReteNodeRecipe) { | ||
324 | if (eObject2 instanceof ReteNodeRecipe) { | ||
325 | EList<Long> eqClassIDs1 = ((ReteNodeRecipe) eObject1).getEquivalenceClassIDs(); | ||
326 | EList<Long> eqClassIDs2 = ((ReteNodeRecipe) eObject2).getEquivalenceClassIDs(); | ||
327 | |||
328 | if (!Collections.disjoint(eqClassIDs1, eqClassIDs2)) | ||
329 | return true; | ||
330 | } | ||
331 | } | ||
332 | |||
333 | // fallback to general comparison | ||
334 | return super.equals(eObject1, eObject2); | ||
335 | } | ||
336 | } | ||
204 | } | 337 | } |
diff --git a/subprojects/interpreter-rete-recipes/src/main/resources/model/recipes.ecore b/subprojects/interpreter-rete-recipes/src/main/resources/model/recipes.ecore index a4617833..6b8f10ea 100644 --- a/subprojects/interpreter-rete-recipes/src/main/resources/model/recipes.ecore +++ b/subprojects/interpreter-rete-recipes/src/main/resources/model/recipes.ecore | |||
@@ -31,6 +31,8 @@ | |||
31 | <details key="documentation" value="If two recipes were found equivalent, a matching equivalence ID can be assigned to them by {@link RecipeRecognizer}. 
If two recipes share (at least one) equivalence ID, they are known to be equivalent.

<p>
A difference in this attribute only does not preclude two recipe elements to be considered equal. 
If they are shown to be equivalent using deeper analysis, equivalence ids can be set so that the equivalence is recognized more easily the next time.

@since 1.3"/> | 31 | <details key="documentation" value="If two recipes were found equivalent, a matching equivalence ID can be assigned to them by {@link RecipeRecognizer}. 
If two recipes share (at least one) equivalence ID, they are known to be equivalent.

<p>
A difference in this attribute only does not preclude two recipe elements to be considered equal. 
If they are shown to be equivalent using deeper analysis, equivalence ids can be set so that the equivalence is recognized more easily the next time.

@since 1.3"/> |
32 | </eAnnotations> | 32 | </eAnnotations> |
33 | </eStructuralFeatures> | 33 | </eStructuralFeatures> |
34 | <eStructuralFeatures xsi:type="ecore:EAttribute" name="cachedHashCode" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//ELongObject"/> | ||
35 | <eStructuralFeatures xsi:type="ecore:EAttribute" name="constructed" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"/> | ||
34 | </eClassifiers> | 36 | </eClassifiers> |
35 | <eClassifiers xsi:type="ecore:EClass" name="SingleParentNodeRecipe" abstract="true" | 37 | <eClassifiers xsi:type="ecore:EClass" name="SingleParentNodeRecipe" abstract="true" |
36 | eSuperTypes="#//ReteNodeRecipe"> | 38 | eSuperTypes="#//ReteNodeRecipe"> |
diff --git a/subprojects/interpreter-rete-recipes/src/main/resources/model/rete-recipes.genmodel b/subprojects/interpreter-rete-recipes/src/main/resources/model/rete-recipes.genmodel index 89ff6e3a..f7dc7a4e 100644 --- a/subprojects/interpreter-rete-recipes/src/main/resources/model/rete-recipes.genmodel +++ b/subprojects/interpreter-rete-recipes/src/main/resources/model/rete-recipes.genmodel | |||
@@ -2,10 +2,11 @@ | |||
2 | <genmodel:GenModel xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" | 2 | <genmodel:GenModel xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" |
3 | xmlns:genmodel="http://www.eclipse.org/emf/2002/GenModel" copyrightText="Copyright (c) 2004-2014 Gabor Bergmann and Daniel Varro
Copyright (c) 2023 The Refinery Authors <https://refinery.tools>
This program and the accompanying materials are made available under the
terms of the Eclipse Public License v. 2.0 which is available at
http://www.eclipse.org/legal/epl-v20.html.

SPDX-License-Identifier: EPL-2.0" | 3 | xmlns:genmodel="http://www.eclipse.org/emf/2002/GenModel" copyrightText="Copyright (c) 2004-2014 Gabor Bergmann and Daniel Varro
Copyright (c) 2023 The Refinery Authors <https://refinery.tools>
This program and the accompanying materials are made available under the
terms of the Eclipse Public License v. 2.0 which is available at
http://www.eclipse.org/legal/epl-v20.html.

SPDX-License-Identifier: EPL-2.0" |
4 | modelDirectory="/tools.refinery.refinery-interpreter-rete-recipes/src/main/emf-gen" | 4 | modelDirectory="/tools.refinery.refinery-interpreter-rete-recipes/src/main/emf-gen" |
5 | modelPluginID="tools.refinery.refinery-interpreter-rete-recipes" runtimeJar="true" forceOverwrite="true" | 5 | modelPluginID="tools.refinery.refinery-interpreter-rete-recipes" runtimeJar="true" |
6 | modelName="Rete-recipes" updateClasspath="false" nonNLSMarkers="true" rootExtendsClass="org.eclipse.emf.ecore.impl.MinimalEObjectImpl$Container" | 6 | forceOverwrite="true" modelName="Rete-recipes" updateClasspath="false" nonNLSMarkers="true" |
7 | testsDirectory="" importerID="org.eclipse.emf.importer.ecore" containmentProxies="true" | 7 | rootExtendsClass="org.eclipse.emf.ecore.impl.MinimalEObjectImpl$Container" testsDirectory="" |
8 | complianceLevel="7.0" language="en" operationReflection="true"> | 8 | importerID="org.eclipse.emf.importer.ecore" containmentProxies="true" complianceLevel="7.0" |
9 | language="en" operationReflection="true"> | ||
9 | <genAnnotations source="http://www.eclipse.org/emf/2002/GenModel/exporter/org.eclipse.xsd.ecore.exporter"> | 10 | <genAnnotations source="http://www.eclipse.org/emf/2002/GenModel/exporter/org.eclipse.xsd.ecore.exporter"> |
10 | <genAnnotations source="selectedPackages"> | 11 | <genAnnotations source="selectedPackages"> |
11 | <details key="http://www.eclipse.org/emf/2002/Ecore" value="Ecore.xsd"/> | 12 | <details key="http://www.eclipse.org/emf/2002/Ecore" value="Ecore.xsd"/> |
@@ -32,6 +33,8 @@ | |||
32 | <genClasses image="false" ecoreClass="recipes.ecore#//ReteNodeRecipe"> | 33 | <genClasses image="false" ecoreClass="recipes.ecore#//ReteNodeRecipe"> |
33 | <genFeatures createChild="false" ecoreFeature="ecore:EAttribute recipes.ecore#//ReteNodeRecipe/traceInfo"/> | 34 | <genFeatures createChild="false" ecoreFeature="ecore:EAttribute recipes.ecore#//ReteNodeRecipe/traceInfo"/> |
34 | <genFeatures createChild="false" ecoreFeature="ecore:EAttribute recipes.ecore#//ReteNodeRecipe/equivalenceClassIDs"/> | 35 | <genFeatures createChild="false" ecoreFeature="ecore:EAttribute recipes.ecore#//ReteNodeRecipe/equivalenceClassIDs"/> |
36 | <genFeatures createChild="false" ecoreFeature="ecore:EAttribute recipes.ecore#//ReteNodeRecipe/cachedHashCode"/> | ||
37 | <genFeatures createChild="false" ecoreFeature="ecore:EAttribute recipes.ecore#//ReteNodeRecipe/constructed"/> | ||
35 | <genOperations ecoreOperation="recipes.ecore#//ReteNodeRecipe/getArity" body="throw new <%java.lang.UnsupportedOperationException%>();"/> | 38 | <genOperations ecoreOperation="recipes.ecore#//ReteNodeRecipe/getArity" body="throw new <%java.lang.UnsupportedOperationException%>();"/> |
36 | </genClasses> | 39 | </genClasses> |
37 | <genClasses image="false" ecoreClass="recipes.ecore#//SingleParentNodeRecipe"> | 40 | <genClasses image="false" ecoreClass="recipes.ecore#//SingleParentNodeRecipe"> |
diff --git a/subprojects/interpreter-rete/src/main/java/tools/refinery/interpreter/rete/network/NodeProvisioner.java b/subprojects/interpreter-rete/src/main/java/tools/refinery/interpreter/rete/network/NodeProvisioner.java index 0a68f449..e7901822 100644 --- a/subprojects/interpreter-rete/src/main/java/tools/refinery/interpreter/rete/network/NodeProvisioner.java +++ b/subprojects/interpreter-rete/src/main/java/tools/refinery/interpreter/rete/network/NodeProvisioner.java | |||
@@ -1,5 +1,6 @@ | |||
1 | /******************************************************************************* | 1 | /******************************************************************************* |
2 | * Copyright (c) 2004-2008 Gabor Bergmann and Daniel Varro | 2 | * Copyright (c) 2004-2008 Gabor Bergmann and Daniel Varro |
3 | * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * This program and the accompanying materials are made available under the | 4 | * This program and the accompanying materials are made available under the |
4 | * terms of the Eclipse Public License v. 2.0 which is available at | 5 | * terms of the Eclipse Public License v. 2.0 which is available at |
5 | * http://www.eclipse.org/legal/epl-v20.html. | 6 | * http://www.eclipse.org/legal/epl-v20.html. |
@@ -74,49 +75,28 @@ public class NodeProvisioner { | |||
74 | public synchronized Address<? extends Node> getOrCreateNodeByRecipe(RecipeTraceInfo recipeTrace) { | 75 | public synchronized Address<? extends Node> getOrCreateNodeByRecipe(RecipeTraceInfo recipeTrace) { |
75 | ReteNodeRecipe recipe = recipeTrace.getRecipe(); | 76 | ReteNodeRecipe recipe = recipeTrace.getRecipe(); |
76 | Address<? extends Node> result = getNodesByRecipe().get(recipe); | 77 | Address<? extends Node> result = getNodesByRecipe().get(recipe); |
77 | if (result != null) { | 78 | if (result == null) { |
78 | // NODE ALREADY CONSTRUCTED FOR RECIPE, only needs to add trace | ||
79 | if (getRecipeTraces().add(recipeTrace)) | ||
80 | result.getNodeCache().assignTraceInfo(recipeTrace); | ||
81 | } else { | ||
82 | // No node for this recipe object - but equivalent recipes still | 79 | // No node for this recipe object - but equivalent recipes still |
83 | // reusable | 80 | // reusable |
84 | ReteNodeRecipe canonicalRecipe = recognizer.canonicalizeRecipe(recipe); | 81 | ReteNodeRecipe canonicalRecipe = recognizer.canonicalizeRecipe(recipe); |
85 | if (canonicalRecipe != recipe) { | 82 | result = getNodesByRecipe().get(canonicalRecipe); |
86 | // FOUND EQUIVALENT RECIPE | 83 | if (result == null) { |
87 | result = getNodesByRecipe().get(canonicalRecipe); | 84 | if (canonicalRecipe.isConstructed()) { |
88 | if (result != null) { | 85 | throw new IllegalStateException( |
89 | // NODE ALREADY CONSTRUCTED FOR EQUIVALENT RECIPE | 86 | "Already constructed node is missing for canonical recipe " + canonicalRecipe); |
90 | recipeTrace.shadowWithEquivalentRecipe(canonicalRecipe); | 87 | } |
91 | getNodesByRecipe().put(recipe, result); | 88 | canonicalRecipe.setConstructed(true); |
92 | if (getRecipeTraces().add(recipeTrace)) | 89 | final Node freshNode = instantiateNodeForRecipe(recipeTrace, canonicalRecipe); |
93 | result.getNodeCache().assignTraceInfo(recipeTrace); | 90 | result = reteContainer.makeAddress(freshNode); |
94 | // Bug 491922: ensure that recipe shadowing propagates to | 91 | } |
95 | // parent traces | 92 | if (canonicalRecipe != recipe) { |
96 | // note that if equivalentRecipes() becomes more | 93 | recipeTrace.shadowWithEquivalentRecipe(canonicalRecipe); |
97 | // sophisticated | 94 | getNodesByRecipe().put(recipe, result); |
98 | // and considers recipes with different parents, this might | 95 | } |
99 | // have to be changed | ||
100 | ensureParents(recipeTrace); | ||
101 | } else { | ||
102 | // CONSTRUCTION IN PROGRESS FOR EQUIVALENT RECIPE | ||
103 | if (recipe instanceof IndexerRecipe) { | ||
104 | // this is allowed for indexers; | ||
105 | // go on with the construction, as the same indexer node | ||
106 | // will be obtained anyways | ||
107 | } else { | ||
108 | throw new IllegalStateException( | ||
109 | "This should not happen: " + "non-indexer nodes are are supposed to be constructed " | ||
110 | + "as soon as they are designated as canonical recipes"); | ||
111 | } | ||
112 | } | ||
113 | } | ||
114 | if (result == null) { | ||
115 | // MUST INSTANTIATE NEW NODE FOR RECIPE | ||
116 | final Node freshNode = instantiateNodeForRecipe(recipeTrace, recipe); | ||
117 | result = reteContainer.makeAddress(freshNode); | ||
118 | } | ||
119 | } | 96 | } |
97 | if (getRecipeTraces().add(recipeTrace)) { | ||
98 | result.getNodeCache().assignTraceInfo(recipeTrace); | ||
99 | } | ||
120 | return result; | 100 | return result; |
121 | } | 101 | } |
122 | 102 | ||