Skip to content

Commit 4723a12

Browse files
authored
Reorganize how Solution handles pinned states and constraints. (#1373)
Reorganize how Solution handles pinned states and constraints. In the process of turning ListElement into a Piece, I ran into a couple of bugs in how pinned states and constraints between pieces are handled. The tests didn't catch those bugs because there are basically two bugs that cancel out. The IfPiece was incorrectly pinning an empty else block in an if statement to split. But the solver was incorrectly ignoring pinned states in some cases. This fixes both bugs. With this change, when generating a Solution, we eagerly bind as many Pieces to States as we can when the Solution is first created before running CodeWriter. To do that, we find all of the pinned Pieces and bind them to their pinned States, then propagate any constraints from that to other child Pieces. This fixes a couple of bugs, but also, I think, leads to a cleaner module where at Solution creation time, we fill in as many piece states as we can based on what we know statically about the Piece tree (its pinned states and their constraints). Then during solution exploring and line splitting, we don't have to worry about pinned states at all. This will, I think, align with future optimizations where we do some other piece pinning before running the solver.
1 parent e68ae72 commit 4723a12

File tree

5 files changed

+107
-60
lines changed

5 files changed

+107
-60
lines changed

lib/src/back_end/code_writer.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ class CodeWriter {
208208
_options.add(_PieceOptions(piece, _options.lastOrNull?.indent ?? 0,
209209
_options.lastOrNull?.allowNewlines ?? true));
210210

211-
var isUnsolved = !_solution.isBound(piece) && piece.states.length > 1;
211+
var isUnsolved =
212+
!_solution.isBound(piece) && piece.additionalStates.isNotEmpty;
212213
if (isUnsolved) _currentUnsolvedPieces.add(piece);
213214

214215
// TODO(perf): Memoize this. Might want to create a nested PieceWriter

lib/src/back_end/solution.dart

Lines changed: 85 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,30 @@ class Solution implements Comparable<Solution> {
9797
/// Creates a new [Solution] with no pieces set to any state (which
9898
/// implicitly means they have state [State.unsplit] unless they're pinned to
9999
/// another state).
100-
Solution(Piece root, int pageWidth) : this._(root, pageWidth, 0, {});
100+
factory Solution(Piece root, int pageWidth) {
101+
var pieceStates = <Piece, State>{};
102+
var cost = 0;
103+
104+
// Bind every pinned piece to its state and propagate any constraints from
105+
// those.
106+
void traversePinned(Piece piece) {
107+
if (piece.pinnedState case var pinned?) {
108+
var additionalCost = _tryBind(pieceStates, piece, pinned);
109+
110+
// Pieces should be implemented such that they never get pinned into a
111+
// conflicting state because then there's no possible solution, so
112+
// [_tryBind()] should always succeed and [additionalCost] won't be
113+
// `null`.
114+
cost += additionalCost!;
115+
}
116+
117+
piece.forEachChild(traversePinned);
118+
}
119+
120+
traversePinned(root);
121+
122+
return Solution._(root, pageWidth, cost, pieceStates);
123+
}
101124

102125
Solution._(Piece root, int pageWidth, this.cost, this._pieceStates) {
103126
var writer = CodeWriter(pageWidth, this);
@@ -111,7 +134,7 @@ class Solution implements Comparable<Solution> {
111134
/// The state this solution selects for [piece].
112135
///
113136
/// If no state has been selected, defaults to the first state.
114-
State pieceState(Piece piece) => _pieceStates[piece] ?? piece.states.first;
137+
State pieceState(Piece piece) => _pieceStates[piece] ?? State.unsplit;
115138

116139
/// Whether [piece] has been bound to a state in this set.
117140
bool isBound(Piece piece) => _pieceStates.containsKey(piece);
@@ -147,8 +170,11 @@ class Solution implements Comparable<Solution> {
147170
_invalidPiece = piece;
148171
}
149172

150-
/// When called on a [Solution] with some unselected piece states, chooses a
151-
/// piece and yields further solutions for each state that piece can have.
173+
/// Derives new potential solutions from this one by binding
174+
/// [_nextPieceToExpand] to all of its possible states.
175+
///
176+
/// If there is no potential piece to expand, or all attempts to expand it
177+
/// fail, returns an empty list.
152178
List<Solution> expand(Piece root, int pageWidth) {
153179
// If the piece whose newline constraint was violated is already bound to
154180
// one state, then every solution derived from this one will also fail in
@@ -176,51 +202,25 @@ class Solution implements Comparable<Solution> {
176202
// make any noticeable performance difference on the one pathological
177203
// example I tried. Leaving this here as a TODO to investigate more when
178204
// there are other benchmarks we can try.
205+
var solutions = <Solution>[];
179206

180-
return [
181-
for (var state in expandPiece.states)
182-
if (_tryBind(root, pageWidth, expandPiece, state) case var solution?)
183-
solution
184-
];
185-
}
186-
187-
/// Attempts to extend this solution's piece states by binding [piece] to
188-
/// [state], taking into account any constraints pieces place on each other.
189-
///
190-
/// Returns a new [Solution] with [piece] bound to [state] and any other
191-
/// pieces constrained by that choice bound to their constrained values
192-
/// (recursively). Returns `null` if a constraint conflicts with the already
193-
/// bound or pinned state for some piece.
194-
Solution? _tryBind(Piece root, int pageWidth, Piece piece, State state) {
195-
var conflict = false;
196-
var newStates = {..._pieceStates};
197-
var newCost = cost;
207+
// For each state that the expanding piece can be in, create a new solution
208+
// that inherits all of the bindings of this one, and binds the expanding
209+
// piece to that state (along with any further pieces constrained by that
210+
// one).
211+
for (var state in expandPiece.states) {
212+
var newStates = {..._pieceStates};
198213

199-
void bind(Piece thisPiece, State thisState) {
200-
// If we've already failed from a previous sibling's constraint violation,
201-
// early out.
202-
if (conflict) return;
203-
204-
// If this piece is already pinned or bound to some other state, then the
205-
// solution doesn't make sense.
206-
var alreadyBound = thisPiece.pinnedState ?? newStates[thisPiece];
207-
if (alreadyBound != null && alreadyBound != thisState) {
208-
conflict = true;
209-
return;
210-
}
214+
var additionalCost = _tryBind(newStates, expandPiece, state);
211215

212-
newStates[thisPiece] = thisState;
213-
newCost += thisPiece.stateCost(thisState);
216+
// Discard the solution if we hit a constraint violation.
217+
if (additionalCost == null) continue;
214218

215-
// This piece may in turn place further constraints on others.
216-
thisPiece.applyConstraints(thisState, bind);
219+
solutions
220+
.add(Solution._(root, pageWidth, cost + additionalCost, newStates));
217221
}
218222

219-
bind(piece, state);
220-
221-
if (conflict) return null;
222-
223-
return Solution._(root, pageWidth, newCost, newStates);
223+
return solutions;
224224
}
225225

226226
/// Compares two solutions where a more desirable solution comes first.
@@ -260,4 +260,46 @@ class Solution implements Comparable<Solution> {
260260
states,
261261
].join(' ');
262262
}
263+
264+
/// Attempts to add a binding from [piece] to [state] in [boundStates], and
265+
/// then adds any further bindings from constraints that [piece] applies to
266+
/// its children, recursively.
267+
///
268+
/// This may fail if [piece] is already bound to a different [state], or if
269+
/// any constrained pieces are bound to different states.
270+
///
271+
/// If successful, returns the additional cost required to bind [piece] to
272+
/// [state] (along with any other applied constrained pieces). Otherwise,
273+
/// returns `null` to indicate failure.
274+
static int? _tryBind(
275+
Map<Piece, State> boundStates, Piece piece, State state) {
276+
var success = true;
277+
var additionalCost = 0;
278+
279+
void bind(Piece thisPiece, State thisState) {
280+
// If we've already failed from a previous sibling's constraint violation,
281+
// early out.
282+
if (!success) return;
283+
284+
// Apply the new binding if it doesn't conflict with an existing one.
285+
switch (boundStates[thisPiece]) {
286+
case null:
287+
// Binding a unbound piece to a state.
288+
additionalCost += thisPiece.stateCost(thisState);
289+
boundStates[thisPiece] = thisState;
290+
291+
// This piece may in turn place further constraints on others.
292+
thisPiece.applyConstraints(thisState, bind);
293+
case var alreadyBound when alreadyBound != thisState:
294+
// Already bound to a different state, so there's a conflict.
295+
success = false;
296+
default:
297+
break; // Already bound to the same state, so nothing to do.
298+
}
299+
}
300+
301+
bind(piece, state);
302+
303+
return success ? additionalCost : null;
304+
}
263305
}

lib/src/front_end/ast_node_visitor.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
857857

858858
@override
859859
Piece visitIfElement(IfElement node) {
860-
var piece = IfPiece();
860+
var piece = IfPiece(isStatement: false);
861861

862862
// Recurses through the else branches to flatten them into a linear if-else
863863
// chain handled by a single [IfPiece].
@@ -940,7 +940,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
940940

941941
@override
942942
Piece visitIfStatement(IfStatement node) {
943-
var piece = IfPiece();
943+
var piece = IfPiece(isStatement: true);
944944

945945
// Recurses through the else branches to flatten them into a linear if-else
946946
// chain handled by a single [IfPiece].
@@ -1840,7 +1840,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
18401840

18411841
var body = nodePiece(node.body);
18421842

1843-
var piece = IfPiece();
1843+
var piece = IfPiece(isStatement: true);
18441844
piece.add(condition, body, isBlock: node.body is Block);
18451845
return piece;
18461846
}

lib/src/piece/if.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import 'piece.dart';
1010
/// We also use this for while statements, which are formatted exactly like an
1111
/// if statement with no else clause.
1212
class IfPiece extends Piece {
13+
/// Whether this is an if statement versus if collection element.
14+
final bool _isStatement;
15+
1316
final List<_IfSection> _sections = [];
1417

18+
IfPiece({required bool isStatement}) : _isStatement = isStatement;
19+
1520
void add(Piece header, Piece statement, {required bool isBlock}) {
1621
_sections.add(_IfSection(header, statement, isBlock));
1722
}
@@ -24,11 +29,12 @@ class IfPiece extends Piece {
2429
// If an if element, any spread collection's split state must follow the
2530
// surrounding if element's: we either split all the spreads or none of
2631
// them. And if any of the non-spread then or else branches split, then the
27-
// spreads do too. This has no effect on if statements since blocks always
28-
// split.
29-
for (var section in _sections) {
30-
if (section.isBlock) {
31-
constrain(section.statement, state);
32+
// spreads do too.
33+
if (!_isStatement) {
34+
for (var section in _sections) {
35+
if (section.isBlock) {
36+
constrain(section.statement, state);
37+
}
3238
}
3339
}
3440
}

lib/src/piece/piece.dart

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@ typedef Constrain = void Function(Piece other, State constrainedState);
1616
abstract class Piece {
1717
/// The ordered list of ways this piece may split.
1818
///
19-
/// If the piece is aready pinned, then this is just the one pinned state.
20-
/// Otherwise, it's [State.unsplit], which all pieces support, followed by
21-
/// any other [additionalStates].
19+
/// This is [State.unsplit], which all pieces support, followed by any other
20+
/// [additionalStates].
2221
List<State> get states {
23-
if (_pinnedState case var state?) {
24-
return [state];
25-
} else {
26-
return [State.unsplit, ...additionalStates];
27-
}
22+
// Pinned pieces should be bound eagerly in the Solution so we shouldn't
23+
// ever need to iterate over their states.
24+
assert(_pinnedState == null);
25+
return [State.unsplit, ...additionalStates];
2826
}
2927

3028
/// The ordered list of all possible ways this piece could split.

0 commit comments

Comments
 (0)