aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-10-14 14:54:38 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-10-14 14:54:38 +0200
commit54c4c91097c50f6cedb02cb5b1aea32ae16a3421 (patch)
tree222b86f87511582c2b602c896e210d1b138220a9
parentfix(query-interpreter): register Recipes package (diff)
downloadrefinery-54c4c91097c50f6cedb02cb5b1aea32ae16a3421.tar.gz
refinery-54c4c91097c50f6cedb02cb5b1aea32ae16a3421.tar.zst
refinery-54c4c91097c50f6cedb02cb5b1aea32ae16a3421.zip
refactor(interpreter-rete): recipe hashing
Use isomorphism-aware hashing to speed up RecipeRecognizer. Due to possibly cyclic (recursive) recipes, we can't recursively turn recipes into a canonical form. Recipes referring to already canonical (non-recurisve, or recursive and entirely canonicalized) recipes can be hashed, while recursive recipes in the middle of caninicalization still have to be compared more slowly by their contents. To keep the list of recipes compared by contents small, recipes are assigned a hash code whenever possible. We keep the equivalence class IDs for recipes, as there might be hash code clashes, as well as recursive recipes that only later get assigned a hash code. Also fixes a concurrency problem with equivalence class IDs by using an AtomicLong. Also reworks recipe instantiation, as now recipes might be canonicalized before they are instantiated.
-rw-r--r--subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java487
-rw-r--r--subprojects/interpreter-rete-recipes/src/main/resources/model/recipes.ecore2
-rw-r--r--subprojects/interpreter-rete-recipes/src/main/resources/model/rete-recipes.genmodel11
-rw-r--r--subprojects/interpreter-rete/src/main/java/tools/refinery/interpreter/rete/network/NodeProvisioner.java58
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 @@
9package tools.refinery.interpreter.rete.recipes.helper; 10package tools.refinery.interpreter.rete.recipes.helper;
10 11
11import org.eclipse.emf.common.util.EList; 12import org.eclipse.emf.common.util.EList;
12import org.eclipse.emf.ecore.EAttribute; 13import org.eclipse.emf.ecore.*;
13import org.eclipse.emf.ecore.EClass;
14import org.eclipse.emf.ecore.EObject;
15import org.eclipse.emf.ecore.EStructuralFeature;
16import org.eclipse.emf.ecore.util.EcoreUtil; 14import org.eclipse.emf.ecore.util.EcoreUtil;
15import org.jetbrains.annotations.Nullable;
17import tools.refinery.interpreter.rete.recipes.RecipesPackage; 16import tools.refinery.interpreter.rete.recipes.RecipesPackage;
18import tools.refinery.interpreter.rete.recipes.ReteNodeRecipe; 17import tools.refinery.interpreter.rete.recipes.ReteNodeRecipe;
19import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext; 18import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext;
20 19
21import java.util.*; 20import java.util.*;
21import 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 */
31public class RecipeRecognizer { 30public 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}. &#xD;&#xA;If two recipes share (at least one) equivalence ID, they are known to be equivalent.&#xD;&#xA;&#xD;&#xA;&lt;p>&#xD;&#xA;A difference in this attribute only does not preclude two recipe elements to be considered equal. &#xD;&#xA;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.&#xD;&#xA;&#xD;&#xA;@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}. &#xD;&#xA;If two recipes share (at least one) equivalence ID, they are known to be equivalent.&#xD;&#xA;&#xD;&#xA;&lt;p>&#xD;&#xA;A difference in this attribute only does not preclude two recipe elements to be considered equal. &#xD;&#xA;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.&#xD;&#xA;&#xD;&#xA;@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&#xA;Copyright (c) 2023 The Refinery Authors &lt;https://refinery.tools>&#xA;This program and the accompanying materials are made available under the&#xA;terms of the Eclipse Public License v. 2.0 which is available at&#xA;http://www.eclipse.org/legal/epl-v20.html.&#xA;&#xA;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&#xA;Copyright (c) 2023 The Refinery Authors &lt;https://refinery.tools>&#xA;This program and the accompanying materials are made available under the&#xA;terms of the Eclipse Public License v. 2.0 which is available at&#xA;http://www.eclipse.org/legal/epl-v20.html.&#xA;&#xA;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 &lt;%java.lang.UnsupportedOperationException%>();"/> 38 <genOperations ecoreOperation="recipes.ecore#//ReteNodeRecipe/getArity" body="throw new &lt;%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