Skip to content

Commit 3e8eeef

Browse files
feat: Support accessing non-declarative shadow variables from a fact (#1621)
1 parent f1f1d07 commit 3e8eeef

17 files changed

+725
-46
lines changed

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DefaultShadowVariableSessionFactory.java

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package ai.timefold.solver.core.impl.domain.variable.declarative;
22

33
import java.util.ArrayList;
4+
import java.util.Collections;
45
import java.util.HashMap;
6+
import java.util.IdentityHashMap;
57
import java.util.LinkedHashSet;
68
import java.util.List;
79
import java.util.Map;
@@ -51,7 +53,7 @@ public static <Solution_> VariableReferenceGraph<Solution_> buildGraph(
5153
// Create variable processors for each declarative shadow variable descriptor
5254
for (var declarativeShadowVariable : declarativeShadowVariableDescriptors) {
5355
var fromVariableId = declarativeShadowVariable.getVariableMetaModel();
54-
createSourceChangeProcessors(variableReferenceGraphBuilder, declarativeShadowVariable, fromVariableId);
56+
createSourceChangeProcessors(entities, variableReferenceGraphBuilder, declarativeShadowVariable, fromVariableId);
5557
var aliasSet = declarativeShadowVariableToAliasMap.get(fromVariableId);
5658
if (aliasSet != null) {
5759
createAliasToVariableChangeProcessors(variableReferenceGraphBuilder, aliasSet, fromVariableId);
@@ -96,6 +98,7 @@ public static <Solution_> VariableReferenceGraph<Solution_> buildGraph(
9698
}
9799

98100
private static <Solution_> void createSourceChangeProcessors(
101+
Object[] entities,
99102
VariableReferenceGraphBuilder<Solution_> variableReferenceGraphBuilder,
100103
DeclarativeShadowVariableDescriptor<Solution_> declarativeShadowVariable,
101104
VariableMetaModel<Solution_, ?, ?> fromVariableId) {
@@ -108,19 +111,33 @@ private static <Solution_> void createSourceChangeProcessors(
108111
// non-declarative variables are not in the graph and must have their
109112
// own processor
110113
if (!sourcePart.isDeclarative()) {
111-
variableReferenceGraphBuilder.addAfterProcessor(toVariableId, (graph, entity) -> {
112-
// Exploits the fact the source entity and the target entity must be the same,
113-
// since non-declarative variables can only be accessed from the root entity;
114-
// paths like "otherVisit.previous" or "visitGroup[].otherVisit.previous" are not allowed,
115-
// but paths like "previous" or "visitGroup[].previous" are.
116-
// Without this invariant, an inverse set must be calculated
117-
// and maintained,
118-
// and this code is complicated enough.
119-
var changed = graph.lookupOrNull(fromVariableId, entity);
120-
if (changed != null) {
121-
graph.markChanged(changed);
114+
if (sourcePart.onRootEntity()) {
115+
// No need for inverse set; source and target entity are the same.
116+
variableReferenceGraphBuilder.addAfterProcessor(toVariableId, (graph, entity) -> {
117+
var changed = graph.lookupOrNull(fromVariableId, entity);
118+
if (changed != null) {
119+
graph.markChanged(changed);
120+
}
121+
});
122+
} else {
123+
// Need to create an inverse set from source to target
124+
var inverseMap = new IdentityHashMap<Object, List<Object>>();
125+
var visitor = source.getEntityVisitor(sourcePart.chainToVariableEntity());
126+
for (var rootEntity : entities) {
127+
if (declarativeShadowVariable.getEntityDescriptor().getEntityClass().isInstance(rootEntity)) {
128+
visitor.accept(rootEntity, shadowEntity -> inverseMap
129+
.computeIfAbsent(shadowEntity, ignored -> new ArrayList<>()).add(rootEntity));
130+
}
122131
}
123-
});
132+
variableReferenceGraphBuilder.addAfterProcessor(toVariableId, (graph, entity) -> {
133+
for (var item : inverseMap.getOrDefault(entity, Collections.emptyList())) {
134+
var changed = graph.lookupOrNull(fromVariableId, item);
135+
if (changed != null) {
136+
graph.markChanged(changed);
137+
}
138+
}
139+
});
140+
}
124141
}
125142
}
126143
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
public record RootVariableSource<Entity_, Value_>(
2525
Class<? extends Entity_> rootEntity,
26+
List<MemberAccessor> listMemberAccessors,
2627
BiConsumer<Object, Consumer<Value_>> valueEntityFunction,
2728
List<VariableSourceReference> variableSourceReferences) {
2829

@@ -44,7 +45,6 @@ private record VariablePath(Class<?> variableEntityClass,
4445
}
4546
return currentEntity;
4647
}
47-
4848
}
4949

5050
public static Iterator<PathPart> pathIterator(Class<?> rootEntity, String path) {
@@ -134,6 +134,7 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
134134
List<MemberAccessor> chainToVariableEntity = chainToVariable.subList(0, chainToVariable.size() - 1);
135135
if (!hasListMemberAccessor) {
136136
valueEntityFunction = getRegularSourceEntityVisitor(chainToVariableEntity);
137+
listMemberAccessors.clear();
137138
} else {
138139
valueEntityFunction = getCollectionSourceEntityVisitor(listMemberAccessors, chainToVariableEntity);
139140
}
@@ -142,7 +143,8 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
142143
for (var i = 0; i < chainStartingFromSourceVariableList.size(); i++) {
143144
var chainStartingFromSourceVariable = chainStartingFromSourceVariableList.get(i);
144145
var newSourceReference =
145-
createVariableSourceReferenceFromChain(solutionMetaModel,
146+
createVariableSourceReferenceFromChain(variablePath, variableSourceReferences, listMemberAccessors,
147+
solutionMetaModel,
146148
rootEntityClass, targetVariableName, chainStartingFromSourceVariable,
147149
chainToVariable,
148150
i == 0,
@@ -167,10 +169,19 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
167169
}
168170

169171
return new RootVariableSource<>(rootEntityClass,
172+
listMemberAccessors,
170173
valueEntityFunction,
171174
variableSourceReferences);
172175
}
173176

177+
public @NonNull BiConsumer<Object, Consumer<Object>> getEntityVisitor(List<MemberAccessor> chainToEntity) {
178+
if (listMemberAccessors.isEmpty()) {
179+
return getRegularSourceEntityVisitor(chainToEntity);
180+
} else {
181+
return getCollectionSourceEntityVisitor(listMemberAccessors, chainToEntity);
182+
}
183+
}
184+
174185
private static <Value_> @NonNull BiConsumer<Object, Consumer<Value_>> getRegularSourceEntityVisitor(
175186
List<MemberAccessor> finalChainToVariable) {
176187
return (entity, consumer) -> {
@@ -197,6 +208,8 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
197208
}
198209

199210
private static <Entity_> @NonNull VariableSourceReference createVariableSourceReferenceFromChain(
211+
String variablePath, List<VariableSourceReference> variableSourceReferences,
212+
List<MemberAccessor> listMemberAccessors,
200213
PlanningSolutionMetaModel<?> solutionMetaModel,
201214
Class<? extends Entity_> rootEntityClass, String targetVariableName, List<MemberAccessor> afterChain,
202215
List<MemberAccessor> chainToVariable, boolean isTopLevel, boolean isBottomLevel) {
@@ -213,13 +226,29 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
213226
solutionMetaModel.entity(maybeDownstreamVariable.getDeclaringClass())
214227
.variable(maybeDownstreamVariable.getName());
215228
}
229+
var isDeclarative = isDeclarativeShadowVariable(variableMemberAccessor);
230+
if (!isDeclarative) {
231+
for (var previousVariableSourceReference : variableSourceReferences) {
232+
if (!previousVariableSourceReference.isDeclarative()) {
233+
throw new IllegalArgumentException(
234+
"""
235+
The source path (%s) starting from root entity class (%s) \
236+
accesses a non-declarative shadow variable (%s) \
237+
after another non-declarative shadow variable (%s)."""
238+
.formatted(
239+
variablePath, rootEntityClass.getSimpleName(), variableMemberAccessor.getName(),
240+
previousVariableSourceReference.variableMetaModel().name()));
241+
}
242+
}
243+
}
216244

217245
return new VariableSourceReference(
218246
solutionMetaModel.entity(variableMemberAccessor.getDeclaringClass()).variable(variableMemberAccessor.getName()),
219247
sourceVariablePath.memberAccessorsBeforeEntity,
248+
isTopLevel && sourceVariablePath.memberAccessorsBeforeEntity.isEmpty() && listMemberAccessors.isEmpty(),
220249
isTopLevel,
221250
isBottomLevel,
222-
isDeclarativeShadowVariable(variableMemberAccessor),
251+
isDeclarative,
223252
solutionMetaModel.entity(rootEntityClass).variable(targetVariableName),
224253
downstreamDeclarativeVariable,
225254
sourceVariablePath::findTargetEntity);
@@ -238,12 +267,6 @@ private static void assertIsValidVariableReference(Class<?> rootEntityClass, Str
238267
variableSourceReference.downstreamDeclarativeVariableMetamodel().name(),
239268
sourceVariableId.name()));
240269
}
241-
if (!variableSourceReference.isDeclarative() && !variableSourceReference.chainToVariableEntity().isEmpty()) {
242-
throw new IllegalArgumentException(
243-
"The source path (%s) starting from root entity class (%s) accesses a non-declarative shadow variable (%s) not from the root entity or collection."
244-
.formatted(variablePath, rootEntityClass.getSimpleName(),
245-
variableSourceReference.variableMetaModel().name()));
246-
}
247270
}
248271

249272
public static Member getMember(Class<?> rootClass, String sourcePath, Class<?> declaringClass,

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/VariableSourceReference.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
@NullMarked
1313
public record VariableSourceReference(VariableMetaModel<?, ?, ?> variableMetaModel,
1414
List<MemberAccessor> chainToVariableEntity,
15+
boolean onRootEntity,
1516
boolean isTopLevel,
1617
boolean isBottomLevel,
1718
boolean isDeclarative,

core/src/test/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSourceTest.java

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ void pathUsingBuiltinShadow() {
8181
assertEmptyChainToVariableEntity(source);
8282
assertThat(source.variableMetaModel()).isEqualTo(previousElementMetaModel);
8383
assertThat(source.isTopLevel()).isTrue();
84+
assertThat(source.onRootEntity()).isTrue();
8485
assertThat(source.isDeclarative()).isFalse();
8586
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
8687
assertThat(source.downstreamDeclarativeVariableMetamodel()).isNull();
@@ -114,6 +115,7 @@ void pathUsingDeclarativeShadow() {
114115
assertEmptyChainToVariableEntity(source);
115116
assertThat(source.variableMetaModel()).isEqualTo(dependencyMetaModel);
116117
assertThat(source.isTopLevel()).isTrue();
118+
assertThat(source.onRootEntity()).isTrue();
117119
assertThat(source.isDeclarative()).isTrue();
118120
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
119121
assertThat(source.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
@@ -146,6 +148,7 @@ void pathUsingDeclarativeShadowAfterGroup() {
146148
assertEmptyChainToVariableEntity(source);
147149
assertThat(source.variableMetaModel()).isEqualTo(dependencyMetaModel);
148150
assertThat(source.isTopLevel()).isTrue();
151+
assertThat(source.onRootEntity()).isFalse();
149152
assertThat(source.isDeclarative()).isTrue();
150153
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
151154
assertThat(source.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
@@ -183,6 +186,7 @@ void pathUsingBuiltinShadowAfterGroup() {
183186
assertEmptyChainToVariableEntity(source);
184187
assertThat(source.variableMetaModel()).isEqualTo(previousElementMetaModel);
185188
assertThat(source.isTopLevel()).isTrue();
189+
assertThat(source.onRootEntity()).isFalse();
186190
assertThat(source.isDeclarative()).isFalse();
187191
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
188192
assertThat(source.downstreamDeclarativeVariableMetamodel()).isNull();
@@ -220,6 +224,7 @@ void pathUsingDeclarativeShadowAfterGroupAfterFact() {
220224
assertEmptyChainToVariableEntity(source);
221225
assertThat(source.variableMetaModel()).isEqualTo(dependencyMetaModel);
222226
assertThat(source.isTopLevel()).isTrue();
227+
assertThat(source.onRootEntity()).isFalse();
223228
assertThat(source.isDeclarative()).isTrue();
224229
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
225230
assertThat(source.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
@@ -259,6 +264,7 @@ void pathUsingDeclarativeShadowAfterBuiltinShadow() {
259264
assertEmptyChainToVariableEntity(previousSource);
260265
assertThat(previousSource.variableMetaModel()).isEqualTo(previousElementMetaModel);
261266
assertThat(previousSource.isTopLevel()).isTrue();
267+
assertThat(previousSource.onRootEntity()).isTrue();
262268
assertThat(previousSource.isDeclarative()).isFalse();
263269
assertThat(previousSource.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
264270
assertThat(previousSource.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
@@ -285,6 +291,44 @@ void pathUsingDeclarativeShadowAfterBuiltinShadow() {
285291
verifyNoMoreInteractions(rootVisitor);
286292
}
287293

294+
@Test
295+
void pathUsingBuiltinShadowAfterFact() {
296+
var rootVariableSource = RootVariableSource.from(
297+
planningSolutionMetaModel,
298+
TestdataInvalidDeclarativeValue.class,
299+
"shadow",
300+
"fact.previous",
301+
DEFAULT_MEMBER_ACCESSOR_FACTORY,
302+
DEFAULT_DESCRIPTOR_POLICY);
303+
304+
assertThat(rootVariableSource.rootEntity()).isEqualTo(TestdataInvalidDeclarativeValue.class);
305+
assertThat(rootVariableSource.variableSourceReferences()).hasSize(1);
306+
var previousSource = rootVariableSource.variableSourceReferences().get(0);
307+
308+
assertChainToVariableEntity(previousSource, "fact");
309+
assertThat(previousSource.variableMetaModel()).isEqualTo(previousElementMetaModel);
310+
assertThat(previousSource.onRootEntity()).isFalse();
311+
assertThat(previousSource.isTopLevel()).isTrue();
312+
assertThat(previousSource.isDeclarative()).isFalse();
313+
assertThat(previousSource.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
314+
assertThat(previousSource.downstreamDeclarativeVariableMetamodel()).isNull();
315+
316+
var previousElement = new TestdataInvalidDeclarativeValue("previous");
317+
var factElement = new TestdataInvalidDeclarativeValue("fact");
318+
var currentElement = new TestdataInvalidDeclarativeValue("current");
319+
320+
factElement.setPrevious(previousElement);
321+
currentElement.setFact(factElement);
322+
323+
var result = previousSource.targetEntityFunctionStartingFromVariableEntity().apply(factElement);
324+
assertThat(result).isSameAs(factElement);
325+
326+
var rootVisitor = mock(Consumer.class);
327+
rootVariableSource.valueEntityFunction().accept(currentElement, rootVisitor);
328+
verify(rootVisitor).accept(factElement);
329+
verifyNoMoreInteractions(rootVisitor);
330+
}
331+
288332
@Test
289333
@SuppressWarnings("unchecked")
290334
void pathUsingDeclarativeShadowAfterBuiltinShadowAfterGroup() {
@@ -303,6 +347,7 @@ void pathUsingDeclarativeShadowAfterBuiltinShadowAfterGroup() {
303347
assertEmptyChainToVariableEntity(previousSource);
304348
assertThat(previousSource.variableMetaModel()).isEqualTo(previousElementMetaModel);
305349
assertThat(previousSource.isTopLevel()).isTrue();
350+
assertThat(previousSource.onRootEntity()).isFalse();
306351
assertThat(previousSource.isDeclarative()).isFalse();
307352
assertThat(previousSource.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
308353
assertThat(previousSource.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
@@ -312,6 +357,7 @@ void pathUsingDeclarativeShadowAfterBuiltinShadowAfterGroup() {
312357
assertChainToVariableEntity(dependencySource, "previous");
313358
assertThat(dependencySource.variableMetaModel()).isEqualTo(dependencyMetaModel);
314359
assertThat(dependencySource.isTopLevel()).isFalse();
360+
assertThat(dependencySource.onRootEntity()).isFalse();
315361
assertThat(dependencySource.isDeclarative()).isTrue();
316362
assertThat(dependencySource.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
317363
assertThat(dependencySource.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
@@ -362,23 +408,7 @@ void invalidPathUsingBuiltinShadowAfterBuiltinShadow() {
362408
.hasMessageContaining("The source path (previous.previous)" +
363409
" starting from root entity class (TestdataInvalidDeclarativeValue)" +
364410
" accesses a non-declarative shadow variable (previous)" +
365-
" not from the root entity or collection.");
366-
}
367-
368-
@Test
369-
void invalidPathUsingBuiltinShadowAfterFact() {
370-
assertThatCode(() -> RootVariableSource.from(
371-
planningSolutionMetaModel,
372-
TestdataInvalidDeclarativeValue.class,
373-
"shadow",
374-
"fact.previous",
375-
DEFAULT_MEMBER_ACCESSOR_FACTORY,
376-
DEFAULT_DESCRIPTOR_POLICY))
377-
.isInstanceOf(IllegalArgumentException.class)
378-
.hasMessageContaining("The source path (fact.previous)" +
379-
" starting from root entity class (TestdataInvalidDeclarativeValue)" +
380-
" accesses a non-declarative shadow variable (previous)" +
381-
" not from the root entity or collection.");
411+
" after another non-declarative shadow variable (previous).");
382412
}
383413

384414
@Test

0 commit comments

Comments
 (0)