Skip to content

Commit 7d5dcfc

Browse files
authored
Fix bug in subtree merging cost calculation. (#1377)
An important optimization during solving is hoisting out a separable Piece subtree, formatting in its own isolated Solver, and then merging the resulting Solution back into the parent Solution. When we merge, we add in all of the data from the subtree to the main one. It's important that doing so yields the exact same result that we would get if we had solved that subtree directly inline in the parent Solution. Unfortunately, the previous code didn't do that. In some cases, pieces that are bound by the subtree Solution would already be bound in the parent one too. In that case, merging would double-count the cost of those pieces. This fixes that by only counting the cost of pieces that weren't already bound in the parent. I ran into this bug while running the large benchmark and it was surprisingly hard to hoist out a small separable test case. The issue wasn't really specific to any particular Piece or syntax. It's really a regression test. The regression tests aren't migrated for the new formatter yet, so I made a new directory "regression_tall" and put it there. I'm not sure if that's ultimately the naming convention we'll want. Also, while I was investigating this, I noticed that ListPiece could theoretically try to format a child Piece separately when doing so wasn't valid because there might be code from a sibling or parent piece on its line. (In practice, that can't happen because the only SequencePiece that doesn't have surrounding brackets is the one top-level sequence for the compilation unit. So there definitely won't be anything before or after that. But if we ever use SequencePiece in other places without brackets, this avoids that becoming a bug.)
1 parent cd0f2ab commit 7d5dcfc

File tree

4 files changed

+40
-5
lines changed

4 files changed

+40
-5
lines changed

lib/src/back_end/solution.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,15 @@ class Solution implements Comparable<Solution> {
168168
/// the resulting solution is being merged with this one.
169169
void mergeSubtree(Solution subtreeSolution) {
170170
_overflow += subtreeSolution._overflow;
171-
_cost += subtreeSolution.cost;
172-
_pieceStates.addAll(subtreeSolution._pieceStates);
171+
172+
// Add the subtree's bound pieces to this one. Make sure to not double
173+
// count costs for pieces that are already bound in this one.
174+
subtreeSolution._pieceStates.forEach((piece, state) {
175+
_pieceStates.putIfAbsent(piece, () {
176+
_cost += piece.stateCost(state);
177+
return state;
178+
});
179+
});
173180
}
174181

175182
/// Sets [selectionStart] to be [start] code units into the output.

lib/src/piece/sequence.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,14 @@ class SequencePiece extends Piece {
4040
for (var i = 0; i < _elements.length; i++) {
4141
var element = _elements[i];
4242

43-
// If the sequence is split, then every element is on its own line and
44-
// can be formatted separately.
45-
writer.format(element, separate: state == State.split);
43+
// We can format an element separately if the element is on its own line.
44+
// This happens when the sequence is split and there is something before
45+
// and after the element, either brackets or other items.
46+
var separate = state == State.split &&
47+
(i > 0 || _leftBracket != null) &&
48+
(i < _elements.length - 1 || _rightBracket != null);
49+
50+
writer.format(element, separate: separate);
4651

4752
if (i < _elements.length - 1) {
4853
writer.newline(

test/regression_tall/other/misc.unit

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
>>> Incorrect cost calculation when merging subtrees led to wrong solution.
2+
SomeVeryLongReturnType
3+
someFunctionName(SomeType_parameter1 AnotherType_parameter2) {
4+
{
5+
deps.then(() {
6+
;
7+
})
8+
.then(________traverseDeps_depender__deps_);
9+
}
10+
}
11+
<<<
12+
SomeVeryLongReturnType someFunctionName(
13+
SomeType_parameter1 AnotherType_parameter2,
14+
) {
15+
{
16+
deps
17+
.then(() {
18+
;
19+
})
20+
.then(________traverseDeps_depender__deps_);
21+
}
22+
}

test/tall_format_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ void main() async {
2121
await testDirectory('top_level', tall: true);
2222
await testDirectory('type', tall: true);
2323
await testDirectory('variable', tall: true);
24+
await testDirectory('regression_tall', tall: true);
2425

2526
test('throws a FormatterException on failed parse', () {
2627
var formatter = DartFormatter();

0 commit comments

Comments
 (0)