Skip to content

Commit fc1a229

Browse files
Refactor how piece constraints are applied. (#1343)
Refactor how piece constraints are applied. When I added support for constructors, I introduced a little API to let pieces enforce constraints between their state and the states of other child pieces. It worked but felt a little bolted on. In particular, enforcing the constraints during CodeWriter formatting meant we didn't prune invalid solutions as efficiently as we could. I'm working on if elements now and I needed a better way to handle constraints between pieces for that, so I went ahead and refactored the way this works to be, I think, generally better: - Enforce constraints between pieces during solution expansion instead of during formatting. This means that we eagerly discard any solution (and the whole tree of solutions that could have come from it) if it doesn't meet the constraints. - Remove the `_pieces` fields in PieceStateSet. With the previous optimization to track the next unsolved piece in Solution, that list wasn't doing much of anything. It's only use (aside from debug output) was for determining the order that pieces are compared when comparing two solutions. But we can get that from iterating over the _pieceStates map since maps in Dart track their key insertion order. - Update ConstructorPiece to use this new API. Co-authored-by: Nate Bosch <[email protected]>
1 parent 5da04ea commit fc1a229

File tree

5 files changed

+100
-105
lines changed

5 files changed

+100
-105
lines changed

lib/src/back_end/code_writer.dart

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,9 @@ class CodeWriter {
5353
/// The number of characters in the line currently being written.
5454
int _column = 0;
5555

56-
/// Whether this solution has encountered a conflict that makes it not a valid
57-
/// solution.
58-
///
59-
/// This can occur if:
60-
///
61-
/// - A mandatory newline occurs like from a line comment or statement where
62-
/// none is permitted.
63-
/// - An outer piece requires a child piece to have a certain state and it
64-
/// doesn't.
65-
bool _isInvalid = false;
56+
/// Whether this solution has encountered a mandatory newline (like from a
57+
/// line comment or a statement terminator) where no newline is permitted.
58+
bool _hasInvalidNewline = false;
6659

6760
/// The stack of state for each [Piece] being formatted.
6861
///
@@ -132,7 +125,7 @@ class CodeWriter {
132125

133126
return Solution(_pieceStates, _buffer.toString(), _selectionStart,
134127
_selectionEnd, _nextPieceToExpand,
135-
isValid: !_isInvalid, overflow: _overflow, cost: _cost);
128+
isValid: !_hasInvalidNewline, overflow: _overflow, cost: _cost);
136129
}
137130

138131
/// Notes that a newline has been written.
@@ -144,7 +137,7 @@ class CodeWriter {
144137
/// the raw text contains a newline, which can happen in multi-line block
145138
/// comments and multi-line string literals.
146139
void handleNewline() {
147-
if (!_options.allowNewlines) _isInvalid = true;
140+
if (!_options.allowNewlines) _hasInvalidNewline = true;
148141

149142
// Note that this piece contains a newline so that we can propagate that
150143
// up to containing pieces too.
@@ -234,32 +227,13 @@ class CodeWriter {
234227

235228
/// Format [piece] and insert the result into the code being written and
236229
/// returned by [finish()].
237-
///
238-
/// If [requireState] is given, then [piece] must be in that state or the
239-
/// solution is considered invalid. This is used to create constraints
240-
/// between pieces. For example, a constructor piece forces the initializer
241-
/// list child piece to split if the parameter list splits.
242-
void format(Piece piece, {State? requireState}) {
230+
void format(Piece piece) {
243231
_pieceOptions.add(_PieceOptions(_options.indent, _options.allowNewlines));
244232

245233
var isUnsolved = !_pieceStates.isBound(piece) && piece.states.length > 1;
246234
if (isUnsolved) _currentUnsolvedPieces.add(piece);
247235

248236
var state = _pieceStates.pieceState(piece);
249-
250-
// If the parent piece constrains this child to a certain state, then
251-
// invalidate the solution if it doesn't match.
252-
if (requireState != null && state != requireState) {
253-
_isInvalid = true;
254-
// TODO(perf): The solver doesn't discard invalid states that are caused
255-
// by newlines because sometimes an invalid state like that can lead to
256-
// a valid state by selecting values for other pieces.
257-
//
258-
// But in this case, once a partial solution is invalidated by one piece's
259-
// state conflicting with another's, any solution derived from that is
260-
// also going to be invalid and we can prune that entire solution tree.
261-
}
262-
263237
_cost += piece.stateCost(state);
264238

265239
// TODO(perf): Memoize this. Might want to create a nested PieceWriter
@@ -336,7 +310,7 @@ class CodeWriter {
336310
// expand it next.
337311
if (!_foundExpandLine &&
338312
_nextPieceToExpand != null &&
339-
(_column > _pageWidth || _isInvalid)) {
313+
(_column > _pageWidth || _hasInvalidNewline)) {
340314
// We found a problematic line, so remember it and the piece on it.
341315
_foundExpandLine = true;
342316
} else if (!_foundExpandLine) {

lib/src/back_end/solution.dart

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,19 @@ import 'code_writer.dart';
77
/// A possibly incomplete set of selected states for a set of pieces being
88
/// solved.
99
class PieceStateSet {
10-
// TODO(perf): Looking up and expanding the set of chunk states was a
11-
// performance bottleneck in the old line splitter. If that turns out to be
12-
// true here, then consider a faster representation for this list and the
13-
// subsequent map field.
14-
/// The in-order flattened list of all pieces being solved.
10+
/// The states that pieces have been bound to.
1511
///
16-
/// This doesn't include pieces like text that have only a single value since
17-
/// there's nothing to solve for them.
18-
final List<Piece> _pieces;
19-
12+
/// Note that order that keys are inserted into this map is significant. When
13+
/// ordering solutions, we use the order that pieces are bound in here to
14+
/// break ties between solutions that otherwise have the same cost and
15+
/// overflow.
2016
final Map<Piece, State> _pieceStates;
2117

2218
/// Creates a new [PieceStateSet] with no pieces set to any state (which
2319
/// implicitly means they have state 0).
24-
PieceStateSet(this._pieces) : _pieceStates = {};
20+
PieceStateSet() : _pieceStates = {};
2521

26-
PieceStateSet._(this._pieces, this._pieceStates);
22+
PieceStateSet._(this._pieceStates);
2723

2824
/// The state this solution selects for [piece].
2925
///
@@ -33,18 +29,43 @@ class PieceStateSet {
3329
/// Whether [piece] has been bound to a state in this set.
3430
bool isBound(Piece piece) => _pieceStates.containsKey(piece);
3531

36-
/// Creates a clone of this state with [piece] bound to [state].
37-
PieceStateSet cloneWith(Piece piece, State state) {
38-
return PieceStateSet._(_pieces, {..._pieceStates, piece: state});
32+
/// Attempts to bind [piece] to [state], taking into account any constraints
33+
/// pieces place on each other.
34+
///
35+
/// Returns a new [PieceStateSet] with [piece] bound to [state] and any other
36+
/// pieces constrained by that choice bound to their constrained values
37+
/// (recursively). Returns `null` if a constraint conflicts with the already
38+
/// bound or pinned state for some piece.
39+
PieceStateSet? tryBind(Piece piece, State state) {
40+
var conflict = false;
41+
var boundStates = {..._pieceStates};
42+
43+
void traverse(Piece thisPiece, State thisState) {
44+
// If this piece is already pinned or bound to some other state, then the
45+
// solution doesn't make sense.
46+
var alreadyBound = thisPiece.pinnedState ?? boundStates[thisPiece];
47+
if (alreadyBound != null && alreadyBound != thisState) {
48+
conflict = true;
49+
return;
50+
}
51+
52+
boundStates[thisPiece] = thisState;
53+
54+
// This piece may in turn place further constraints on others.
55+
thisPiece.applyConstraints(thisState, traverse);
56+
}
57+
58+
traverse(piece, state);
59+
60+
if (conflict) return null;
61+
return PieceStateSet._(boundStates);
3962
}
4063

4164
@override
4265
String toString() {
43-
return _pieces.map((piece) {
44-
var state = _pieceStates[piece];
45-
var stateLabel = state == null ? '?' : '$state';
46-
return '$piece:$stateLabel';
47-
}).join(' ');
66+
return _pieceStates.keys
67+
.map((piece) => '$piece:${_pieceStates[piece]}')
68+
.join(' ');
4869
}
4970
}
5071

@@ -54,7 +75,7 @@ class PieceStateSet {
5475
/// code and its cost.
5576
class Solution implements Comparable<Solution> {
5677
/// The states the pieces have been set to in this solution.
57-
final PieceStateSet _state;
78+
final PieceStateSet _stateSet;
5879

5980
/// The formatted code.
6081
final String text;
@@ -119,8 +140,8 @@ class Solution implements Comparable<Solution> {
119140
/// no selection.
120141
final int? selectionEnd;
121142

122-
factory Solution.initial(Piece root, int pageWidth, List<Piece> pieces) {
123-
return Solution._(root, pageWidth, PieceStateSet(pieces));
143+
factory Solution.initial(Piece root, int pageWidth) {
144+
return Solution._(root, pageWidth, PieceStateSet());
124145
}
125146

126147
factory Solution._(Piece root, int pageWidth, PieceStateSet state) {
@@ -129,7 +150,7 @@ class Solution implements Comparable<Solution> {
129150
return writer.finish();
130151
}
131152

132-
Solution(this._state, this.text, this.selectionStart, this.selectionEnd,
153+
Solution(this._stateSet, this.text, this.selectionStart, this.selectionEnd,
133154
this._nextPieceToExpand,
134155
{required this.overflow, required this.cost, required this.isValid});
135156

@@ -139,7 +160,8 @@ class Solution implements Comparable<Solution> {
139160
if (_nextPieceToExpand case var piece?) {
140161
return [
141162
for (var state in piece.states)
142-
Solution._(root, pageWidth, _state.cloneWith(piece, state))
163+
if (_stateSet.tryBind(piece, state) case final stateSet?)
164+
Solution._(root, pageWidth, stateSet)
143165
];
144166
}
145167

@@ -161,16 +183,10 @@ class Solution implements Comparable<Solution> {
161183

162184
if (overflow != other.overflow) return overflow.compareTo(other.overflow);
163185

164-
// Should be solving the same set of pieces.
165-
assert(_state._pieces.length == other._state._pieces.length);
166-
167-
// If all else is equal, prefer lower states in earlier pieces.
168-
// TODO(tall): This might not be needed once piece scoring is more
169-
// sophisticated.
170-
for (var i = 0; i < _state._pieces.length; i++) {
171-
var piece = _state._pieces[i];
172-
var thisState = _state.pieceState(piece);
173-
var otherState = other._state.pieceState(piece);
186+
// If all else is equal, prefer lower states in earlier bound pieces.
187+
for (var piece in _stateSet._pieceStates.keys) {
188+
var thisState = _stateSet.pieceState(piece);
189+
var otherState = other._stateSet.pieceState(piece);
174190
if (thisState != otherState) return thisState.compareTo(otherState);
175191
}
176192

@@ -183,7 +199,7 @@ class Solution implements Comparable<Solution> {
183199
'\$$cost',
184200
if (overflow > 0) '($overflow over)',
185201
if (!isValid) '(invalid)',
186-
'$_state',
202+
'$_stateSet',
187203
].join(' ');
188204
}
189205
}

lib/src/back_end/solver.dart

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,27 +34,10 @@ class Solver {
3434

3535
Solver(this._pageWidth);
3636

37-
/// Finds the best set of line splits for [piece] and returns the resulting
38-
/// formatted code.
39-
Solution format(Piece piece) {
40-
// Collect all of the pieces with states that can be selected.
41-
var unsolvedPieces = <Piece>[];
42-
43-
void traverse(Piece piece) {
44-
// We don't need to worry about selecting pieces that have only one state.
45-
if (piece.states.length > 1) unsolvedPieces.add(piece);
46-
piece.forEachChild(traverse);
47-
}
48-
49-
traverse(piece);
50-
51-
return _solve(piece, unsolvedPieces);
52-
}
53-
54-
/// Finds the best solution for the piece tree starting at [root] with
55-
/// selectable [pieces].
56-
Solution _solve(Piece root, List<Piece> pieces) {
57-
var solution = Solution.initial(root, _pageWidth, pieces);
37+
/// Finds the best set of line splits for [root] piece and returns the
38+
/// resulting formatted code.
39+
Solution format(Piece root) {
40+
var solution = Solution.initial(root, _pageWidth);
5841
_queue.add(solution);
5942

6043
// The lowest cost solution found so far that does overflow.

lib/src/piece/constructor.dart

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,30 +107,42 @@ class ConstructorPiece extends Piece {
107107
_splitBetweenInitializers
108108
];
109109

110+
/// Apply constraints between how the parameters may split and how the
111+
/// initializers may split.
110112
@override
111-
void format(CodeWriter writer, State state) {
112-
// There are constraints between how the parameters may split and now the
113-
// initializers may split.
114-
var (parameterState, initializerState) = switch (state) {
115-
// If there are no initializers, the parameters can do whatever.
116-
State.unsplit when _initializers == null => (null, null),
117-
// All parameters and initializers on one line.
118-
State.unsplit => (State.unsplit, State.unsplit),
119-
// If the `:` splits, then the parameters can't.
120-
_splitBeforeInitializers => (State.unsplit, State.split),
121-
// The `) :` on its own line.
122-
_splitBetweenInitializers => (State.split, State.split),
123-
_ => throw ArgumentError(),
124-
};
113+
void applyConstraints(State state, Constrain constrain) {
114+
// If there are no initializers, the parameters can do whatever.
115+
if (_initializers case var initializers?) {
116+
switch (state) {
117+
case State.unsplit:
118+
// All parameters and initializers on one line.
119+
constrain(_parameters, State.unsplit);
120+
constrain(initializers, State.unsplit);
121+
122+
case _splitBeforeInitializers:
123+
// Only split before the `:` when the parameters fit on one line.
124+
constrain(_parameters, State.unsplit);
125+
constrain(initializers, State.split);
126+
127+
case _splitBetweenInitializers:
128+
// Split both the parameters and initializers and put the `) :` on
129+
// its own line.
130+
constrain(_parameters, State.split);
131+
constrain(initializers, State.split);
132+
}
133+
}
134+
}
125135

136+
@override
137+
void format(CodeWriter writer, State state) {
126138
// If there's a newline in the header or parameters (like a line comment
127139
// after the `)`), then don't allow the initializers to remain unsplit.
128140
if (_initializers != null && state == State.unsplit) {
129141
writer.setAllowNewlines(false);
130142
}
131143

132144
writer.format(_header);
133-
writer.format(_parameters, requireState: parameterState);
145+
writer.format(_parameters);
134146

135147
if (_redirect case var redirect?) {
136148
writer.space();
@@ -153,7 +165,7 @@ class ConstructorPiece extends Piece {
153165
writer.setIndent(4);
154166
}
155167

156-
writer.format(initializers, requireState: initializerState);
168+
writer.format(initializers);
157169
}
158170

159171
writer.setIndent(0);

lib/src/piece/piece.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import '../back_end/code_writer.dart';
66

7+
typedef Constrain = void Function(Piece other, State constrainedState);
8+
79
/// Base class for the formatter's internal representation used for line
810
/// splitting.
911
///
@@ -48,6 +50,14 @@ abstract class Piece {
4850
State? get pinnedState => _pinnedState;
4951
State? _pinnedState;
5052

53+
/// Apply any constraints that this piece places on other pieces when this
54+
/// piece is bound to [state].
55+
///
56+
/// A piece class can override this. For any child piece that it wants to
57+
/// constrain when this piece is in [state], call [constrain] and pass in the
58+
/// child piece and the state that child should be constrained to.
59+
void applyConstraints(State state, Constrain constrain) {}
60+
5161
/// Given that this piece is in [state], use [writer] to produce its formatted
5262
/// output.
5363
void format(CodeWriter writer, State state);

0 commit comments

Comments
 (0)