Skip to content

Commit c08e9d6

Browse files
authored
fix: detect numeric overflow in scoring (#2015)
The score director performance regression tests point towards an overall slowdown, but all of it within the margin of error, therefore the actual difference will be very small. When the overhead of the rest of the solver (and not just score director) comes into play, the impact of this will become effectively zero.
1 parent db55a96 commit c08e9d6

19 files changed

+250
-96
lines changed

core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreContext.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
import org.jspecify.annotations.NullMarked;
1111

12-
final class BendableBigDecimalScoreContext extends ScoreContext<BendableBigDecimalScore, BendableBigDecimalScoreInliner> {
12+
final class BendableBigDecimalScoreContext
13+
extends ScoreContext<BendableBigDecimalScore, BendableBigDecimalScoreInliner> {
1314

1415
private final int hardScoreLevelCount;
1516
private final int softScoreLevelCount;

core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreContext.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ public BendableLongScoreContext(BendableLongScoreInliner parent, AbstractConstra
3232

3333
public ScoreImpact<BendableLongScore> changeSoftScoreBy(long matchWeight,
3434
ConstraintMatchSupplier<BendableLongScore> constraintMatchSupplier) {
35-
var softImpact = scoreLevelWeight * matchWeight;
36-
inliner.softScores[scoreLevel] += softImpact;
35+
var softImpact = Math.multiplyExact(scoreLevelWeight, matchWeight);
36+
inliner.softScores[scoreLevel] = Math.addExact(inliner.softScores[scoreLevel], softImpact);
3737
var scoreImpact = new SingleSoftImpact(this, softImpact);
3838
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
3939
}
4040

4141
public ScoreImpact<BendableLongScore> changeHardScoreBy(long matchWeight,
4242
ConstraintMatchSupplier<BendableLongScore> constraintMatchSupplier) {
43-
var hardImpact = scoreLevelWeight * matchWeight;
44-
inliner.hardScores[scoreLevel] += hardImpact;
43+
var hardImpact = Math.multiplyExact(scoreLevelWeight, matchWeight);
44+
inliner.hardScores[scoreLevel] = Math.addExact(inliner.hardScores[scoreLevel], hardImpact);
4545
var scoreImpact = new SingleHardImpact(this, hardImpact);
4646
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
4747
}
@@ -51,14 +51,14 @@ public ScoreImpact<BendableLongScore> changeScoreBy(long matchWeight,
5151
var hardImpacts = new long[hardScoreLevelCount];
5252
var softImpacts = new long[softScoreLevelCount];
5353
for (var hardScoreLevel = 0; hardScoreLevel < hardScoreLevelCount; hardScoreLevel++) {
54-
var hardImpact = constraintWeight.hardScore(hardScoreLevel) * matchWeight;
54+
var hardImpact = Math.multiplyExact(constraintWeight.hardScore(hardScoreLevel), matchWeight);
5555
hardImpacts[hardScoreLevel] = hardImpact;
56-
inliner.hardScores[hardScoreLevel] += hardImpact;
56+
inliner.hardScores[hardScoreLevel] = Math.addExact(inliner.hardScores[hardScoreLevel], hardImpact);
5757
}
5858
for (var softScoreLevel = 0; softScoreLevel < softScoreLevelCount; softScoreLevel++) {
59-
var softImpact = constraintWeight.softScore(softScoreLevel) * matchWeight;
59+
var softImpact = Math.multiplyExact(constraintWeight.softScore(softScoreLevel), matchWeight);
6060
softImpacts[softScoreLevel] = softImpact;
61-
inliner.softScores[softScoreLevel] += softImpact;
61+
inliner.softScores[softScoreLevel] = Math.addExact(inliner.softScores[softScoreLevel], softImpact);
6262
}
6363
var scoreImpact = new ComplexImpact(this, hardImpacts, softImpacts);
6464
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
@@ -70,7 +70,7 @@ private record SingleSoftImpact(BendableLongScoreContext ctx,
7070

7171
@Override
7272
public void undo() {
73-
ctx.inliner.softScores[ctx.scoreLevel] -= impact;
73+
ctx.inliner.softScores[ctx.scoreLevel] = Math.subtractExact(ctx.inliner.softScores[ctx.scoreLevel], impact);
7474
}
7575

7676
@Override
@@ -85,7 +85,7 @@ private record SingleHardImpact(BendableLongScoreContext ctx,
8585

8686
@Override
8787
public void undo() {
88-
ctx.inliner.hardScores[ctx.scoreLevel] -= impact;
88+
ctx.inliner.hardScores[ctx.scoreLevel] = Math.subtractExact(ctx.inliner.hardScores[ctx.scoreLevel], impact);
8989
}
9090

9191
@Override
@@ -102,10 +102,12 @@ private record ComplexImpact(BendableLongScoreContext ctx, long[] hardImpacts,
102102
public void undo() {
103103
var inliner = ctx.inliner;
104104
for (var hardScoreLevel = 0; hardScoreLevel < ctx.hardScoreLevelCount; hardScoreLevel++) {
105-
inliner.hardScores[hardScoreLevel] -= hardImpacts[hardScoreLevel];
105+
inliner.hardScores[hardScoreLevel] =
106+
Math.subtractExact(inliner.hardScores[hardScoreLevel], hardImpacts[hardScoreLevel]);
106107
}
107108
for (var softScoreLevel = 0; softScoreLevel < ctx.softScoreLevelCount; softScoreLevel++) {
108-
inliner.softScores[softScoreLevel] -= softImpacts[softScoreLevel];
109+
inliner.softScores[softScoreLevel] =
110+
Math.subtractExact(inliner.softScores[softScoreLevel], softImpacts[softScoreLevel]);
109111
}
110112
}
111113

core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreContext.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ public BendableScoreContext(BendableScoreInliner parent, AbstractConstraint<?, ?
3232

3333
public ScoreImpact<BendableScore> changeSoftScoreBy(int matchWeight,
3434
ConstraintMatchSupplier<BendableScore> constraintMatchSupplier) {
35-
var softImpact = scoreLevelWeight * matchWeight;
36-
inliner.softScores[scoreLevel] += softImpact;
35+
var softImpact = Math.multiplyExact(scoreLevelWeight, matchWeight);
36+
inliner.softScores[scoreLevel] = Math.addExact(inliner.softScores[scoreLevel], softImpact);
3737
var scoreImpact = new SingleSoftImpact(this, softImpact);
3838
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
3939
}
4040

4141
public ScoreImpact<BendableScore> changeHardScoreBy(int matchWeight,
4242
ConstraintMatchSupplier<BendableScore> constraintMatchSupplier) {
43-
var hardImpact = scoreLevelWeight * matchWeight;
44-
inliner.hardScores[scoreLevel] += hardImpact;
43+
var hardImpact = Math.multiplyExact(scoreLevelWeight, matchWeight);
44+
inliner.hardScores[scoreLevel] = Math.addExact(inliner.hardScores[scoreLevel], hardImpact);
4545
var scoreImpact = new SingleHardImpact(this, hardImpact);
4646
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
4747
}
@@ -51,14 +51,14 @@ public ScoreImpact<BendableScore> changeScoreBy(int matchWeight,
5151
var hardImpacts = new int[hardScoreLevelCount];
5252
var softImpacts = new int[softScoreLevelCount];
5353
for (var hardScoreLevel = 0; hardScoreLevel < hardScoreLevelCount; hardScoreLevel++) {
54-
var hardImpact = constraintWeight.hardScore(hardScoreLevel) * matchWeight;
54+
var hardImpact = Math.multiplyExact(constraintWeight.hardScore(hardScoreLevel), matchWeight);
5555
hardImpacts[hardScoreLevel] = hardImpact;
56-
inliner.hardScores[hardScoreLevel] += hardImpact;
56+
inliner.hardScores[hardScoreLevel] = Math.addExact(inliner.hardScores[hardScoreLevel], hardImpact);
5757
}
5858
for (var softScoreLevel = 0; softScoreLevel < softScoreLevelCount; softScoreLevel++) {
59-
var softImpact = constraintWeight.softScore(softScoreLevel) * matchWeight;
59+
var softImpact = Math.multiplyExact(constraintWeight.softScore(softScoreLevel), matchWeight);
6060
softImpacts[softScoreLevel] = softImpact;
61-
inliner.softScores[softScoreLevel] += softImpact;
61+
inliner.softScores[softScoreLevel] = Math.addExact(inliner.softScores[softScoreLevel], softImpact);
6262
}
6363
var scoreImpact = new ComplexImpact(this, hardImpacts, softImpacts);
6464
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
@@ -69,7 +69,7 @@ private record SingleSoftImpact(BendableScoreContext ctx, int impact) implements
6969

7070
@Override
7171
public void undo() {
72-
ctx.inliner.softScores[ctx.scoreLevel] -= impact;
72+
ctx.inliner.softScores[ctx.scoreLevel] = Math.subtractExact(ctx.inliner.softScores[ctx.scoreLevel], impact);
7373
}
7474

7575
@Override
@@ -83,7 +83,7 @@ private record SingleHardImpact(BendableScoreContext ctx, int impact) implements
8383

8484
@Override
8585
public void undo() {
86-
ctx.inliner.hardScores[ctx.scoreLevel] -= impact;
86+
ctx.inliner.hardScores[ctx.scoreLevel] = Math.subtractExact(ctx.inliner.hardScores[ctx.scoreLevel], impact);
8787
}
8888

8989
@Override
@@ -100,10 +100,12 @@ private record ComplexImpact(BendableScoreContext ctx, int[] hardImpacts,
100100
public void undo() {
101101
var inliner = ctx.inliner;
102102
for (var hardScoreLevel = 0; hardScoreLevel < ctx.hardScoreLevelCount; hardScoreLevel++) {
103-
inliner.hardScores[hardScoreLevel] -= hardImpacts[hardScoreLevel];
103+
inliner.hardScores[hardScoreLevel] =
104+
Math.subtractExact(inliner.hardScores[hardScoreLevel], hardImpacts[hardScoreLevel]);
104105
}
105106
for (var softScoreLevel = 0; softScoreLevel < ctx.softScoreLevelCount; softScoreLevel++) {
106-
inliner.softScores[softScoreLevel] -= softImpacts[softScoreLevel];
107+
inliner.softScores[softScoreLevel] =
108+
Math.subtractExact(inliner.softScores[softScoreLevel], softImpacts[softScoreLevel]);
107109
}
108110
}
109111

core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreContext.java

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
import org.jspecify.annotations.NullMarked;
77

8-
final class HardMediumSoftLongScoreContext extends ScoreContext<HardMediumSoftLongScore, HardMediumSoftLongScoreInliner> {
8+
final class HardMediumSoftLongScoreContext
9+
extends ScoreContext<HardMediumSoftLongScore, HardMediumSoftLongScoreInliner> {
910

1011
public HardMediumSoftLongScoreContext(HardMediumSoftLongScoreInliner parent, AbstractConstraint<?, ?, ?> constraint,
1112
HardMediumSoftLongScore constraintWeight) {
@@ -14,36 +15,36 @@ public HardMediumSoftLongScoreContext(HardMediumSoftLongScoreInliner parent, Abs
1415

1516
public ScoreImpact<HardMediumSoftLongScore> changeSoftScoreBy(long matchWeight,
1617
ConstraintMatchSupplier<HardMediumSoftLongScore> constraintMatchSupplier) {
17-
var softImpact = constraintWeight.softScore() * matchWeight;
18-
inliner.softScore += softImpact;
18+
var softImpact = Math.multiplyExact(constraintWeight.softScore(), matchWeight);
19+
inliner.softScore = Math.addExact(inliner.softScore, softImpact);
1920
var scoreImpact = new SoftImpact(inliner, softImpact);
2021
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
2122
}
2223

2324
public ScoreImpact<HardMediumSoftLongScore> changeMediumScoreBy(long matchWeight,
2425
ConstraintMatchSupplier<HardMediumSoftLongScore> constraintMatchSupplier) {
25-
var mediumImpact = constraintWeight.mediumScore() * matchWeight;
26-
inliner.mediumScore += mediumImpact;
26+
var mediumImpact = Math.multiplyExact(constraintWeight.mediumScore(), matchWeight);
27+
inliner.mediumScore = Math.addExact(inliner.mediumScore, mediumImpact);
2728
var scoreImpact = new MediumImpact(inliner, mediumImpact);
2829
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
2930
}
3031

3132
public ScoreImpact<HardMediumSoftLongScore> changeHardScoreBy(long matchWeight,
3233
ConstraintMatchSupplier<HardMediumSoftLongScore> constraintMatchSupplier) {
33-
var hardImpact = constraintWeight.hardScore() * matchWeight;
34-
inliner.hardScore += hardImpact;
34+
var hardImpact = Math.multiplyExact(constraintWeight.hardScore(), matchWeight);
35+
inliner.hardScore = Math.addExact(inliner.hardScore, hardImpact);
3536
var scoreImpact = new HardImpact(inliner, hardImpact);
3637
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
3738
}
3839

3940
public ScoreImpact<HardMediumSoftLongScore> changeScoreBy(long matchWeight,
4041
ConstraintMatchSupplier<HardMediumSoftLongScore> constraintMatchSupplier) {
41-
var hardImpact = constraintWeight.hardScore() * matchWeight;
42-
var mediumImpact = constraintWeight.mediumScore() * matchWeight;
43-
var softImpact = constraintWeight.softScore() * matchWeight;
44-
inliner.hardScore += hardImpact;
45-
inliner.mediumScore += mediumImpact;
46-
inliner.softScore += softImpact;
42+
var hardImpact = Math.multiplyExact(constraintWeight.hardScore(), matchWeight);
43+
var mediumImpact = Math.multiplyExact(constraintWeight.mediumScore(), matchWeight);
44+
var softImpact = Math.multiplyExact(constraintWeight.softScore(), matchWeight);
45+
inliner.hardScore = Math.addExact(inliner.hardScore, hardImpact);
46+
inliner.mediumScore = Math.addExact(inliner.mediumScore, mediumImpact);
47+
inliner.softScore = Math.addExact(inliner.softScore, softImpact);
4748
var scoreImpact = new ComplexImpact(inliner, hardImpact, mediumImpact, softImpact);
4849
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
4950
}
@@ -54,7 +55,7 @@ private record SoftImpact(HardMediumSoftLongScoreInliner inliner,
5455

5556
@Override
5657
public void undo() {
57-
inliner.softScore -= softImpact;
58+
inliner.softScore = Math.subtractExact(inliner.softScore, softImpact);
5859
}
5960

6061
@Override
@@ -70,7 +71,7 @@ private record MediumImpact(HardMediumSoftLongScoreInliner inliner,
7071

7172
@Override
7273
public void undo() {
73-
inliner.mediumScore -= mediumImpact;
74+
inliner.mediumScore = Math.subtractExact(inliner.mediumScore, mediumImpact);
7475
}
7576

7677
@Override
@@ -86,7 +87,7 @@ private record HardImpact(HardMediumSoftLongScoreInliner inliner,
8687

8788
@Override
8889
public void undo() {
89-
inliner.hardScore -= hardImpact;
90+
inliner.hardScore = Math.subtractExact(inliner.hardScore, hardImpact);
9091
}
9192

9293
@Override
@@ -102,9 +103,9 @@ private record ComplexImpact(HardMediumSoftLongScoreInliner inliner, long hardIm
102103

103104
@Override
104105
public void undo() {
105-
inliner.hardScore -= hardImpact;
106-
inliner.mediumScore -= mediumImpact;
107-
inliner.softScore -= softImpact;
106+
inliner.hardScore = Math.subtractExact(inliner.hardScore, hardImpact);
107+
inliner.mediumScore = Math.subtractExact(inliner.mediumScore, mediumImpact);
108+
inliner.softScore = Math.subtractExact(inliner.softScore, softImpact);
108109
}
109110

110111
@Override

core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreContext.java

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,36 @@ public HardMediumSoftScoreContext(HardMediumSoftScoreInliner parent, AbstractCon
1414

1515
public ScoreImpact<HardMediumSoftScore> changeSoftScoreBy(int matchWeight,
1616
ConstraintMatchSupplier<HardMediumSoftScore> constraintMatchSupplier) {
17-
var softImpact = constraintWeight.softScore() * matchWeight;
18-
inliner.softScore += softImpact;
17+
var softImpact = Math.multiplyExact(constraintWeight.softScore(), matchWeight);
18+
inliner.softScore = Math.addExact(inliner.softScore, softImpact);
1919
var scoreImpact = new SoftImpact(inliner, softImpact);
2020
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
2121
}
2222

2323
public ScoreImpact<HardMediumSoftScore> changeMediumScoreBy(int matchWeight,
2424
ConstraintMatchSupplier<HardMediumSoftScore> constraintMatchSupplier) {
25-
var mediumImpact = constraintWeight.mediumScore() * matchWeight;
26-
inliner.mediumScore += mediumImpact;
25+
var mediumImpact = Math.multiplyExact(constraintWeight.mediumScore(), matchWeight);
26+
inliner.mediumScore = Math.addExact(inliner.mediumScore, mediumImpact);
2727
var scoreImpact = new MediumImpact(inliner, mediumImpact);
2828
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
2929
}
3030

3131
public ScoreImpact<HardMediumSoftScore> changeHardScoreBy(int matchWeight,
3232
ConstraintMatchSupplier<HardMediumSoftScore> constraintMatchSupplier) {
33-
var hardImpact = constraintWeight.hardScore() * matchWeight;
34-
inliner.hardScore += hardImpact;
33+
var hardImpact = Math.multiplyExact(constraintWeight.hardScore(), matchWeight);
34+
inliner.hardScore = Math.addExact(inliner.hardScore, hardImpact);
3535
var scoreImpact = new HardImpact(inliner, hardImpact);
3636
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
3737
}
3838

3939
public ScoreImpact<HardMediumSoftScore> changeScoreBy(int matchWeight,
4040
ConstraintMatchSupplier<HardMediumSoftScore> constraintMatchSupplier) {
41-
var hardImpact = constraintWeight.hardScore() * matchWeight;
42-
var mediumImpact = constraintWeight.mediumScore() * matchWeight;
43-
var softImpact = constraintWeight.softScore() * matchWeight;
44-
inliner.hardScore += hardImpact;
45-
inliner.mediumScore += mediumImpact;
46-
inliner.softScore += softImpact;
41+
var hardImpact = Math.multiplyExact(constraintWeight.hardScore(), matchWeight);
42+
var mediumImpact = Math.multiplyExact(constraintWeight.mediumScore(), matchWeight);
43+
var softImpact = Math.multiplyExact(constraintWeight.softScore(), matchWeight);
44+
inliner.hardScore = Math.addExact(inliner.hardScore, hardImpact);
45+
inliner.mediumScore = Math.addExact(inliner.mediumScore, mediumImpact);
46+
inliner.softScore = Math.addExact(inliner.softScore, softImpact);
4747
var scoreImpact = new ComplexImpact(inliner, hardImpact, mediumImpact, softImpact);
4848
return possiblyAddConstraintMatch(scoreImpact, constraintMatchSupplier);
4949
}
@@ -54,7 +54,7 @@ private record SoftImpact(HardMediumSoftScoreInliner inliner,
5454

5555
@Override
5656
public void undo() {
57-
inliner.softScore -= softImpact;
57+
inliner.softScore = Math.subtractExact(inliner.softScore, softImpact);
5858
}
5959

6060
@Override
@@ -70,7 +70,7 @@ private record MediumImpact(HardMediumSoftScoreInliner inliner,
7070

7171
@Override
7272
public void undo() {
73-
inliner.mediumScore -= mediumImpact;
73+
inliner.mediumScore = Math.subtractExact(inliner.mediumScore, mediumImpact);
7474
}
7575

7676
@Override
@@ -86,7 +86,7 @@ private record HardImpact(HardMediumSoftScoreInliner inliner,
8686

8787
@Override
8888
public void undo() {
89-
inliner.hardScore -= hardImpact;
89+
inliner.hardScore = Math.subtractExact(inliner.hardScore, hardImpact);
9090
}
9191

9292
@Override
@@ -102,9 +102,9 @@ private record ComplexImpact(HardMediumSoftScoreInliner inliner, int hardImpact,
102102

103103
@Override
104104
public void undo() {
105-
inliner.hardScore -= hardImpact;
106-
inliner.mediumScore -= mediumImpact;
107-
inliner.softScore -= softImpact;
105+
inliner.hardScore = Math.subtractExact(inliner.hardScore, hardImpact);
106+
inliner.mediumScore = Math.subtractExact(inliner.mediumScore, mediumImpact);
107+
inliner.softScore = Math.subtractExact(inliner.softScore, softImpact);
108108
}
109109

110110
@Override

core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreContext.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
import org.jspecify.annotations.NullMarked;
99

10-
final class HardSoftBigDecimalScoreContext extends ScoreContext<HardSoftBigDecimalScore, HardSoftBigDecimalScoreInliner> {
10+
final class HardSoftBigDecimalScoreContext
11+
extends ScoreContext<HardSoftBigDecimalScore, HardSoftBigDecimalScoreInliner> {
1112

1213
public HardSoftBigDecimalScoreContext(HardSoftBigDecimalScoreInliner parent, AbstractConstraint<?, ?, ?> constraint,
1314
HardSoftBigDecimalScore constraintWeight) {

0 commit comments

Comments
 (0)