Skip to content

Commit 860410d

Browse files
authored
Fix two bugs around pinned states in the Solver and SolutionCache. (#1409)
Fix two bugs around pinned states in the Solver and SolutionCache. I was working on an unrelated bug fix and I stumbled into a couple of incorrect formats in some complex examples. After poking around, I found two bugs: - In SolutionCache, it always used the given state of the root Piece even if that state wasn't explicitly bound in the parent Solution. This causes incorrect output if we are separately formatting a child, the parent Solution doesn't bind the child's root, and the best state for the child is *not* the unsplit state. That's a rare enough combination of events that none of the existing tests hit it, but I'll have some new tests for the unrelated bug fix that do. - In Solution, when applying constraints between pieces, it doesn't take into account conflicts from pinned pieces. This bug was introduced by #1407. It wasn't caught because the one test that happens to tickle this doesn't tickle it when a certain child piece is formatted separately, but does if you disable that optimization. The fix to SolutionCache also means we no longer use the root piece's state as part of the cache key. Somewhat surprisingly, it doesn't seem to be necessary. Taking the state out of the cache key is good because it means we're able to reuse more cached Solutions in different contexts. This PR makes the large benchmark about 5% faster.
1 parent 13a3a51 commit 860410d

File tree

6 files changed

+49
-29
lines changed

6 files changed

+49
-29
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.3.6-wip
2+
3+
There are no user-visible changes in this release. The only changes are behind
4+
the `tall-style` experiment flag.
5+
16
## 2.3.5
27

38
* Ensure switch expressions containing line comments split (#1404).

lib/src/back_end/code_writer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ class CodeWriter {
266266
/// writer's [_solution].
267267
void _formatSeparate(Piece piece) {
268268
var solution = _cache.find(
269-
_pageWidth, piece, _solution.pieceState(piece), _pendingIndent);
269+
_pageWidth, piece, _pendingIndent, _solution.pieceStateIfBound(piece));
270270

271271
_pendingIndent = 0;
272272
_flushWhitespace();

lib/src/back_end/solution.dart

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,16 @@ class Solution implements Comparable<Solution> {
128128
_nextPieceToExpand = nextPieceToExpand;
129129
}
130130

131-
/// The state this solution selects for [piece].
131+
/// The state that [piece] is pinned to or that this solution selects.
132132
///
133133
/// If no state has been selected, defaults to the first state.
134-
State pieceState(Piece piece) =>
135-
piece.pinnedState ?? _pieceStates[piece] ?? State.unsplit;
134+
State pieceState(Piece piece) => pieceStateIfBound(piece) ?? State.unsplit;
135+
136+
/// The state that [piece] is pinned to or that this solution selects.
137+
///
138+
/// If no state has been selected, returns `null`.
139+
State? pieceStateIfBound(Piece piece) =>
140+
piece.pinnedState ?? _pieceStates[piece];
136141

137142
/// Whether [piece] has been bound to a state in this set (or is pinned).
138143
bool isBound(Piece piece) =>
@@ -278,7 +283,7 @@ class Solution implements Comparable<Solution> {
278283
if (overflow > 0) '($overflow over)',
279284
if (!isValid) '(invalid)',
280285
states.join(' '),
281-
].join(' ');
286+
].join(' ').trim();
282287
}
283288

284289
/// Attempts to add a binding from [piece] to [state] in [boundStates], and
@@ -302,7 +307,7 @@ class Solution implements Comparable<Solution> {
302307
if (!success) return;
303308

304309
// Apply the new binding if it doesn't conflict with an existing one.
305-
switch (boundStates[thisPiece]) {
310+
switch (thisPiece.pinnedState ?? boundStates[thisPiece]) {
306311
case null:
307312
// Binding a unbound piece to a state.
308313
additionalCost += thisPiece.stateCost(thisState);

lib/src/back_end/solution_cache.dart

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import 'solver.dart';
99
/// Maintains a cache of [Piece] subtrees that have been previously solved.
1010
///
1111
/// If a given [Piece] has newlines before and after it, then (in most cases,
12-
/// assuming there are no other constraints), then the way it is formatted
13-
/// really only depends on its leading indentation and state. In that case, we
14-
/// can format that piece using a separate Solver and insert the results in any
15-
/// Solution that has that piece at that leading indentation.
12+
/// assuming there are no other constraints) the way it is formatted only
13+
/// depends on its leading indentation. In that case, we can format that piece
14+
/// using a separate Solver and insert the results in any Solution that has
15+
/// that piece at that leading indentation.
1616
///
1717
/// This cache stores those previously formatted subtree pieces so that
1818
/// [CodeWriter] can reuse them across [Solution]s.
@@ -27,23 +27,28 @@ class SolutionCache {
2727

2828
/// Returns a previously cached solution for formatting [root] with leading
2929
/// [indent] or produces a new solution, caches it, and returns it.
30-
Solution find(int pageWidth, Piece root, State state, int indent) {
30+
///
31+
/// If [root] is already bound to a state in the surrounding piece tree's
32+
/// [Solution], then [stateIfBound] is that state. Otherwise, it is treated
33+
/// as unbound and the cache will find a state for [root] as well as its
34+
/// children.
35+
Solution find(int pageWidth, Piece root, int indent, State? stateIfBound) {
3136
// See if we've already formatted this piece at this indentation. If not,
3237
// format it and store the result.
3338
return _cache.putIfAbsent(
34-
(root, state, indent: indent),
39+
(root, indent: indent),
3540
() => Solver(this, pageWidth: pageWidth, leadingIndent: indent)
36-
.format(root, state));
41+
.format(root, stateIfBound));
3742
}
3843
}
3944

4045
/// The key used to uniquely identify a previously formatted Piece.
4146
///
42-
/// Each subtree solution depends only on the Piece, the State it's bound to,
43-
/// and amount of leading indentation in the context where it appears (which
44-
/// may vary based on how surrounding pieces end up splitting).
47+
/// Each subtree solution depends only on the Piece and the amount of leading
48+
/// indentation in the context where it appears (which may vary based on how
49+
/// surrounding pieces end up splitting).
4550
///
4651
/// In particular, note that if surrounding pieces split in *different* ways
4752
/// that still end up producing the same overall leading indentation, we are
4853
/// able to reuse a previously cached Solution for some Piece.
49-
typedef _Key = (Piece, State, {int indent});
54+
typedef _Key = (Piece, {int indent});

lib/src/back_end/solver.dart

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,28 +51,32 @@ class Solver {
5151
///
5252
/// If [rootState] is given, then [root] is bound to that state.
5353
Solution format(Piece root, [State? rootState]) {
54-
var solution = Solution(_cache, root,
55-
pageWidth: _pageWidth,
56-
leadingIndent: _leadingIndent,
57-
rootState: rootState);
5854
if (debug.traceSolver) {
5955
var unsolved = <Piece>[];
6056
void traverse(Piece piece) {
61-
if (piece.additionalStates.isNotEmpty &&
62-
piece.pinnedState == null &&
63-
!solution.isBound(piece)) {
64-
unsolved.add(piece);
65-
}
57+
if (piece.states.length > 1) unsolved.add(piece);
6658

6759
piece.forEachChild(traverse);
6860
}
6961

7062
traverse(root);
7163

72-
debug.log(debug.bold('Solving $root for ${unsolved.join(' ')}:'));
64+
var label = [
65+
'Solving $root',
66+
if (rootState != null) 'at state $rootState',
67+
if (unsolved.isNotEmpty) 'for ${unsolved.join(', ')}',
68+
].join(' ');
69+
70+
debug.log(debug.bold('$label:'));
71+
debug.indent();
7372
debug.log(debug.pieceTree(root));
7473
}
7574

75+
var solution = Solution(_cache, root,
76+
pageWidth: _pageWidth,
77+
leadingIndent: _leadingIndent,
78+
rootState: rootState);
79+
7680
_queue.add(solution);
7781

7882
// The lowest cost solution found so far that does overflow.
@@ -87,7 +91,7 @@ class Solver {
8791
tries++;
8892

8993
if (debug.traceSolver) {
90-
debug.log(debug.bold('#$tries $solution'));
94+
debug.log(debug.bold('Try #$tries $solution'));
9195
debug.log(solution.text);
9296
debug.log('');
9397
}
@@ -116,6 +120,7 @@ class Solver {
116120

117121
// If we didn't find a solution without overflow, pick the least bad one.
118122
if (debug.traceSolver) {
123+
debug.unindent();
119124
debug.log(debug.bold('Solved $root to $best:'));
120125
debug.log(best.text);
121126
debug.log('');

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: dart_style
22
# Note: See tool/grind.dart for how to bump the version.
3-
version: 2.3.5
3+
version: 2.3.6-wip
44
description: >-
55
Opinionated, automatic Dart source code formatter.
66
Provides an API and a CLI tool.

0 commit comments

Comments
 (0)