Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -51,7 +53,7 @@ public static <Solution_> VariableReferenceGraph<Solution_> buildGraph(
// Create variable processors for each declarative shadow variable descriptor
for (var declarativeShadowVariable : declarativeShadowVariableDescriptors) {
var fromVariableId = declarativeShadowVariable.getVariableMetaModel();
createSourceChangeProcessors(variableReferenceGraphBuilder, declarativeShadowVariable, fromVariableId);
createSourceChangeProcessors(entities, variableReferenceGraphBuilder, declarativeShadowVariable, fromVariableId);
var aliasSet = declarativeShadowVariableToAliasMap.get(fromVariableId);
if (aliasSet != null) {
createAliasToVariableChangeProcessors(variableReferenceGraphBuilder, aliasSet, fromVariableId);
Expand Down Expand Up @@ -96,6 +98,7 @@ public static <Solution_> VariableReferenceGraph<Solution_> buildGraph(
}

private static <Solution_> void createSourceChangeProcessors(
Object[] entities,
VariableReferenceGraphBuilder<Solution_> variableReferenceGraphBuilder,
DeclarativeShadowVariableDescriptor<Solution_> declarativeShadowVariable,
VariableMetaModel<Solution_, ?, ?> fromVariableId) {
Expand All @@ -108,19 +111,33 @@ private static <Solution_> void createSourceChangeProcessors(
// non-declarative variables are not in the graph and must have their
// own processor
if (!sourcePart.isDeclarative()) {
variableReferenceGraphBuilder.addAfterProcessor(toVariableId, (graph, entity) -> {
// Exploits the fact the source entity and the target entity must be the same,
// since non-declarative variables can only be accessed from the root entity;
// paths like "otherVisit.previous" or "visitGroup[].otherVisit.previous" are not allowed,
// but paths like "previous" or "visitGroup[].previous" are.
// Without this invariant, an inverse set must be calculated
// and maintained,
// and this code is complicated enough.
var changed = graph.lookupOrNull(fromVariableId, entity);
if (changed != null) {
graph.markChanged(changed);
if (sourcePart.onRootEntity()) {
// No need for inverse set; source and target entity are the same.
variableReferenceGraphBuilder.addAfterProcessor(toVariableId, (graph, entity) -> {
var changed = graph.lookupOrNull(fromVariableId, entity);
if (changed != null) {
graph.markChanged(changed);
}
});
} else {
// Need to create an inverse set from source to target
var inverseMap = new IdentityHashMap<Object, List<Object>>();
var visitor = source.getEntityVisitor(sourcePart.chainToVariableEntity());
for (var rootEntity : entities) {
if (declarativeShadowVariable.getEntityDescriptor().getEntityClass().isInstance(rootEntity)) {
visitor.accept(rootEntity, shadowEntity -> inverseMap
.computeIfAbsent(shadowEntity, ignored -> new ArrayList<>()).add(rootEntity));
}
}
});
variableReferenceGraphBuilder.addAfterProcessor(toVariableId, (graph, entity) -> {
for (var item : inverseMap.getOrDefault(entity, Collections.emptyList())) {
var changed = graph.lookupOrNull(fromVariableId, item);
if (changed != null) {
graph.markChanged(changed);
}
}
});
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

public record RootVariableSource<Entity_, Value_>(
Class<? extends Entity_> rootEntity,
List<MemberAccessor> listMemberAccessors,
BiConsumer<Object, Consumer<Value_>> valueEntityFunction,
List<VariableSourceReference> variableSourceReferences) {

Expand All @@ -44,7 +45,6 @@ private record VariablePath(Class<?> variableEntityClass,
}
return currentEntity;
}

}

public static Iterator<PathPart> pathIterator(Class<?> rootEntity, String path) {
Expand Down Expand Up @@ -134,6 +134,7 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
List<MemberAccessor> chainToVariableEntity = chainToVariable.subList(0, chainToVariable.size() - 1);
if (!hasListMemberAccessor) {
valueEntityFunction = getRegularSourceEntityVisitor(chainToVariableEntity);
listMemberAccessors.clear();
} else {
valueEntityFunction = getCollectionSourceEntityVisitor(listMemberAccessors, chainToVariableEntity);
}
Expand All @@ -142,7 +143,8 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
for (var i = 0; i < chainStartingFromSourceVariableList.size(); i++) {
var chainStartingFromSourceVariable = chainStartingFromSourceVariableList.get(i);
var newSourceReference =
createVariableSourceReferenceFromChain(solutionMetaModel,
createVariableSourceReferenceFromChain(variablePath, variableSourceReferences, listMemberAccessors,
solutionMetaModel,
rootEntityClass, targetVariableName, chainStartingFromSourceVariable,
chainToVariable,
i == 0,
Expand All @@ -161,10 +163,19 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
}

return new RootVariableSource<>(rootEntityClass,
listMemberAccessors,
valueEntityFunction,
variableSourceReferences);
}

public @NonNull BiConsumer<Object, Consumer<Object>> getEntityVisitor(List<MemberAccessor> chainToEntity) {
if (listMemberAccessors.isEmpty()) {
return getRegularSourceEntityVisitor(chainToEntity);
} else {
return getCollectionSourceEntityVisitor(listMemberAccessors, chainToEntity);
}
}

private static <Value_> @NonNull BiConsumer<Object, Consumer<Value_>> getRegularSourceEntityVisitor(
List<MemberAccessor> finalChainToVariable) {
return (entity, consumer) -> {
Expand All @@ -191,6 +202,8 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
}

private static <Entity_> @NonNull VariableSourceReference createVariableSourceReferenceFromChain(
String variablePath, List<VariableSourceReference> variableSourceReferences,
List<MemberAccessor> listMemberAccessors,
PlanningSolutionMetaModel<?> solutionMetaModel,
Class<? extends Entity_> rootEntityClass, String targetVariableName, List<MemberAccessor> afterChain,
List<MemberAccessor> chainToVariable, boolean isTopLevel, boolean isBottomLevel) {
Expand All @@ -207,13 +220,29 @@ public static <Entity_, Value_> RootVariableSource<Entity_, Value_> from(
solutionMetaModel.entity(maybeDownstreamVariable.getDeclaringClass())
.variable(maybeDownstreamVariable.getName());
}
var isDeclarative = isDeclarativeShadowVariable(variableMemberAccessor);
if (!isDeclarative) {
for (var previousVariableSourceReference : variableSourceReferences) {
if (!previousVariableSourceReference.isDeclarative()) {
throw new IllegalArgumentException(
"""
The source path (%s) starting from root entity class (%s) \
accesses a non-declarative shadow variable (%s) \
after another non-declarative shadow variable (%s)."""
.formatted(
variablePath, rootEntityClass.getSimpleName(), variableMemberAccessor.getName(),
previousVariableSourceReference.variableMetaModel().name()));
}
}
}

return new VariableSourceReference(
solutionMetaModel.entity(variableMemberAccessor.getDeclaringClass()).variable(variableMemberAccessor.getName()),
sourceVariablePath.memberAccessorsBeforeEntity,
isTopLevel && sourceVariablePath.memberAccessorsBeforeEntity.isEmpty() && listMemberAccessors.isEmpty(),
isTopLevel,
isBottomLevel,
isDeclarativeShadowVariable(variableMemberAccessor),
isDeclarative,
solutionMetaModel.entity(rootEntityClass).variable(targetVariableName),
downstreamDeclarativeVariable,
sourceVariablePath::findTargetEntity);
Expand All @@ -232,12 +261,6 @@ private static void assertIsValidVariableReference(Class<?> rootEntityClass, Str
variableSourceReference.downstreamDeclarativeVariableMetamodel().name(),
sourceVariableId.name()));
}
if (!variableSourceReference.isDeclarative() && !variableSourceReference.chainToVariableEntity().isEmpty()) {
throw new IllegalArgumentException(
"The source path (%s) starting from root entity class (%s) accesses a non-declarative shadow variable (%s) not from the root entity or collection."
.formatted(variablePath, rootEntityClass.getSimpleName(),
variableSourceReference.variableMetaModel().name()));
}
}

public static Member getMember(Class<?> rootClass, String sourcePath, Class<?> declaringClass,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@NullMarked
public record VariableSourceReference(VariableMetaModel<?, ?, ?> variableMetaModel,
List<MemberAccessor> chainToVariableEntity,
boolean onRootEntity,
boolean isTopLevel,
boolean isBottomLevel,
boolean isDeclarative,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ void pathUsingBuiltinShadow() {
assertEmptyChainToVariableEntity(source);
assertThat(source.variableMetaModel()).isEqualTo(previousElementMetaModel);
assertThat(source.isTopLevel()).isTrue();
assertThat(source.onRootEntity()).isTrue();
assertThat(source.isDeclarative()).isFalse();
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(source.downstreamDeclarativeVariableMetamodel()).isNull();
Expand Down Expand Up @@ -114,6 +115,7 @@ void pathUsingDeclarativeShadow() {
assertEmptyChainToVariableEntity(source);
assertThat(source.variableMetaModel()).isEqualTo(dependencyMetaModel);
assertThat(source.isTopLevel()).isTrue();
assertThat(source.onRootEntity()).isTrue();
assertThat(source.isDeclarative()).isTrue();
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(source.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
Expand Down Expand Up @@ -146,6 +148,7 @@ void pathUsingDeclarativeShadowAfterGroup() {
assertEmptyChainToVariableEntity(source);
assertThat(source.variableMetaModel()).isEqualTo(dependencyMetaModel);
assertThat(source.isTopLevel()).isTrue();
assertThat(source.onRootEntity()).isFalse();
assertThat(source.isDeclarative()).isTrue();
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(source.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
Expand Down Expand Up @@ -183,6 +186,7 @@ void pathUsingBuiltinShadowAfterGroup() {
assertEmptyChainToVariableEntity(source);
assertThat(source.variableMetaModel()).isEqualTo(previousElementMetaModel);
assertThat(source.isTopLevel()).isTrue();
assertThat(source.onRootEntity()).isFalse();
assertThat(source.isDeclarative()).isFalse();
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(source.downstreamDeclarativeVariableMetamodel()).isNull();
Expand Down Expand Up @@ -220,6 +224,7 @@ void pathUsingDeclarativeShadowAfterGroupAfterFact() {
assertEmptyChainToVariableEntity(source);
assertThat(source.variableMetaModel()).isEqualTo(dependencyMetaModel);
assertThat(source.isTopLevel()).isTrue();
assertThat(source.onRootEntity()).isFalse();
assertThat(source.isDeclarative()).isTrue();
assertThat(source.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(source.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
Expand Down Expand Up @@ -259,6 +264,7 @@ void pathUsingDeclarativeShadowAfterBuiltinShadow() {
assertEmptyChainToVariableEntity(previousSource);
assertThat(previousSource.variableMetaModel()).isEqualTo(previousElementMetaModel);
assertThat(previousSource.isTopLevel()).isTrue();
assertThat(previousSource.onRootEntity()).isTrue();
assertThat(previousSource.isDeclarative()).isFalse();
assertThat(previousSource.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(previousSource.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
Expand All @@ -285,6 +291,44 @@ void pathUsingDeclarativeShadowAfterBuiltinShadow() {
verifyNoMoreInteractions(rootVisitor);
}

@Test
void pathUsingBuiltinShadowAfterFact() {
var rootVariableSource = RootVariableSource.from(
planningSolutionMetaModel,
TestdataInvalidDeclarativeValue.class,
"shadow",
"fact.previous",
DEFAULT_MEMBER_ACCESSOR_FACTORY,
DEFAULT_DESCRIPTOR_POLICY);

assertThat(rootVariableSource.rootEntity()).isEqualTo(TestdataInvalidDeclarativeValue.class);
assertThat(rootVariableSource.variableSourceReferences()).hasSize(1);
var previousSource = rootVariableSource.variableSourceReferences().get(0);

assertChainToVariableEntity(previousSource, "fact");
assertThat(previousSource.variableMetaModel()).isEqualTo(previousElementMetaModel);
assertThat(previousSource.onRootEntity()).isFalse();
assertThat(previousSource.isTopLevel()).isTrue();
assertThat(previousSource.isDeclarative()).isFalse();
assertThat(previousSource.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(previousSource.downstreamDeclarativeVariableMetamodel()).isNull();

var previousElement = new TestdataInvalidDeclarativeValue("previous");
var factElement = new TestdataInvalidDeclarativeValue("fact");
var currentElement = new TestdataInvalidDeclarativeValue("current");

factElement.setPrevious(previousElement);
currentElement.setFact(factElement);

var result = previousSource.targetEntityFunctionStartingFromVariableEntity().apply(factElement);
assertThat(result).isSameAs(factElement);

var rootVisitor = mock(Consumer.class);
rootVariableSource.valueEntityFunction().accept(currentElement, rootVisitor);
verify(rootVisitor).accept(factElement);
verifyNoMoreInteractions(rootVisitor);
}

@Test
@SuppressWarnings("unchecked")
void pathUsingDeclarativeShadowAfterBuiltinShadowAfterGroup() {
Expand All @@ -303,6 +347,7 @@ void pathUsingDeclarativeShadowAfterBuiltinShadowAfterGroup() {
assertEmptyChainToVariableEntity(previousSource);
assertThat(previousSource.variableMetaModel()).isEqualTo(previousElementMetaModel);
assertThat(previousSource.isTopLevel()).isTrue();
assertThat(previousSource.onRootEntity()).isFalse();
assertThat(previousSource.isDeclarative()).isFalse();
assertThat(previousSource.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(previousSource.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
Expand All @@ -312,6 +357,7 @@ void pathUsingDeclarativeShadowAfterBuiltinShadowAfterGroup() {
assertChainToVariableEntity(dependencySource, "previous");
assertThat(dependencySource.variableMetaModel()).isEqualTo(dependencyMetaModel);
assertThat(dependencySource.isTopLevel()).isFalse();
assertThat(dependencySource.onRootEntity()).isFalse();
assertThat(dependencySource.isDeclarative()).isTrue();
assertThat(dependencySource.targetVariableMetamodel()).isEqualTo(shadowVariableMetaModel);
assertThat(dependencySource.downstreamDeclarativeVariableMetamodel()).isEqualTo(dependencyMetaModel);
Expand Down Expand Up @@ -362,23 +408,7 @@ void invalidPathUsingBuiltinShadowAfterBuiltinShadow() {
.hasMessageContaining("The source path (previous.previous)" +
" starting from root entity class (TestdataInvalidDeclarativeValue)" +
" accesses a non-declarative shadow variable (previous)" +
" not from the root entity or collection.");
}

@Test
void invalidPathUsingBuiltinShadowAfterFact() {
assertThatCode(() -> RootVariableSource.from(
planningSolutionMetaModel,
TestdataInvalidDeclarativeValue.class,
"shadow",
"fact.previous",
DEFAULT_MEMBER_ACCESSOR_FACTORY,
DEFAULT_DESCRIPTOR_POLICY))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("The source path (fact.previous)" +
" starting from root entity class (TestdataInvalidDeclarativeValue)" +
" accesses a non-declarative shadow variable (previous)" +
" not from the root entity or collection.");
" after another non-declarative shadow variable (previous).");
}

@Test
Expand Down
Loading
Loading