Skip to content

Commit b6070b4

Browse files
authored
Prune solutions derived from invalid ones. (#1372)
Prune solutions derived from invalid ones. A solution is invalid if a child piece writes a newline at a point in time where a parent disallows newlines. When that happens, the parent's may be explicitly bound to a state by the solution, or it may still be in an unbound state (and thus using the default). If unbound, then even though this solution is invalid, it may lead to later valid ones by binding that parent piece to a different state. But once a solution binds a piece to a state, all solutions derived from that state keep the same binding. So if we hit an invalid solution and the piece whose newline constraint was already violated, we can discard that solution and all possible solutions derived from it. This skips an entire potentially huge subtree from the solution space being explored. In an example giant deeply nested function call from the benchmark, before this optimization, the solver tries 329,057 solutions before finding a winning one. With this optimization, it only takes 5,405.
1 parent 398e8c8 commit b6070b4

File tree

3 files changed

+194
-161
lines changed

3 files changed

+194
-161
lines changed

lib/src/back_end/code_writer.dart

Lines changed: 35 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,11 @@ import 'solution.dart';
1717
/// an instance of this class. It has methods that the piece can call to add
1818
/// output text to the resulting code, recursively format child pieces, insert
1919
/// whitespace, etc.
20-
///
21-
/// This class also accumulates the score (the relative desireability of a set
22-
/// of formatting choices) that the resulting code has by tracking things like
23-
/// how many characters of code overflow the page width.
2420
class CodeWriter {
2521
final int _pageWidth;
2622

27-
/// The state values for the pieces being written.
28-
final PieceStateSet _pieceStates;
23+
/// The solution this [CodeWriter] is generating code for.
24+
final Solution _solution;
2925

3026
/// Buffer for the code being written.
3127
final StringBuffer _buffer = StringBuffer();
@@ -43,20 +39,9 @@ class CodeWriter {
4339
/// [Whitespace.blankLine].
4440
int _pendingIndent = 0;
4541

46-
/// The cost of the currently chosen line splits.
47-
int _cost = 0;
48-
49-
/// The total number of characters of code that have overflowed the page
50-
/// width so far.
51-
int _overflow = 0;
52-
5342
/// The number of characters in the line currently being written.
5443
int _column = 0;
5544

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;
59-
6045
/// The stack of state for each [Piece] being formatted.
6146
///
6247
/// For each piece being formatted from a call to [format()], we keep track of
@@ -67,7 +52,7 @@ class CodeWriter {
6752
///
6853
/// This is used to increase the cumulative nesting as we recurse into pieces
6954
/// and then unwind that as child pieces are completed.
70-
final List<_PieceOptions> _pieceOptions = [_PieceOptions(0, true)];
55+
final List<_PieceOptions> _options = [];
7156

7257
/// Whether we have already found the first line where whose piece should be
7358
/// used to expand further solutions.
@@ -101,31 +86,14 @@ class CodeWriter {
10186
/// solution if the line ends up overflowing.
10287
final List<Piece> _currentUnsolvedPieces = [];
10388

104-
/// The options for the current innermost piece being formatted.
105-
_PieceOptions get _options => _pieceOptions.last;
106-
107-
/// The offset in the formatted code where the selection starts.
108-
///
109-
/// This is `null` until the piece containing the selection start is reached
110-
/// at which point it gets set. It remains `null` if there is no selection.
111-
int? _selectionStart;
112-
113-
/// The offset in the formatted code where the selection ends.
114-
///
115-
/// This is `null` until the piece containing the selection end is reached
116-
/// at which point it gets set. It remains `null` if there is no selection.
117-
int? _selectionEnd;
118-
119-
CodeWriter(this._pageWidth, this._pieceStates);
89+
CodeWriter(this._pageWidth, this._solution);
12090

121-
/// Returns the finished code produced by formatting the tree of pieces and
122-
/// the final score.
123-
Solution finish() {
91+
/// Returns the final formatted text and the next piece that can be expanded
92+
/// from the solution this [CodeWriter] is writing, if any.
93+
(String, Piece?) finish() {
12494
_finishLine();
12595

126-
return Solution(_pieceStates, _buffer.toString(), _selectionStart,
127-
_selectionEnd, _nextPieceToExpand,
128-
isValid: !_hasInvalidNewline, overflow: _overflow, cost: _cost);
96+
return (_buffer.toString(), _nextPieceToExpand);
12997
}
13098

13199
/// Appends [text] to the output.
@@ -158,10 +126,17 @@ class CodeWriter {
158126
/// piece to [indent], relative to the indentation of the surrounding piece.
159127
///
160128
/// Replaces any previous indentation set by this piece.
161-
///
162129
// TODO(tall): Add another API that adds/subtracts existing indentation.
163130
void setIndent(int indent) {
164-
_options.indent = _pieceOptions[_pieceOptions.length - 2].indent + indent;
131+
var parentIndent = 0;
132+
133+
// If there is a surrounding Piece, then set the indent relative to that
134+
// piece's current indentation.
135+
if (_options.length > 1) {
136+
parentIndent = _options[_options.length - 2].indent;
137+
}
138+
139+
_options.last.indent = parentIndent + indent;
165140
}
166141

167142
/// Inserts a newline if [condition] is true.
@@ -215,7 +190,7 @@ class CodeWriter {
215190
void whitespace(Whitespace whitespace, {bool flushLeft = false}) {
216191
if (whitespace case Whitespace.newline || Whitespace.blankLine) {
217192
_handleNewline();
218-
_pendingIndent = flushLeft ? 0 : _options.indent;
193+
_pendingIndent = flushLeft ? 0 : _options.last.indent;
219194
}
220195

221196
_pendingWhitespace = _pendingWhitespace.collapse(whitespace);
@@ -224,31 +199,29 @@ class CodeWriter {
224199
/// Sets whether newlines are allowed to occur from this point on for the
225200
/// current piece.
226201
void setAllowNewlines(bool allowed) {
227-
_options.allowNewlines = allowed;
202+
_options.last.allowNewlines = allowed;
228203
}
229204

230205
/// Format [piece] and insert the result into the code being written and
231206
/// returned by [finish()].
232207
void format(Piece piece) {
233-
_pieceOptions.add(_PieceOptions(_options.indent, _options.allowNewlines));
208+
_options.add(_PieceOptions(piece, _options.lastOrNull?.indent ?? 0,
209+
_options.lastOrNull?.allowNewlines ?? true));
234210

235-
var isUnsolved = !_pieceStates.isBound(piece) && piece.states.length > 1;
211+
var isUnsolved = !_solution.isBound(piece) && piece.states.length > 1;
236212
if (isUnsolved) _currentUnsolvedPieces.add(piece);
237213

238-
var state = _pieceStates.pieceState(piece);
239-
_cost += piece.stateCost(state);
240-
241214
// TODO(perf): Memoize this. Might want to create a nested PieceWriter
242215
// instead of passing in `this` so we can better control what state needs
243216
// to be used as the key in the memoization table.
244-
piece.format(this, state);
217+
piece.format(this, _solution.pieceState(piece));
245218

246219
if (isUnsolved) _currentUnsolvedPieces.removeLast();
247220

248-
var childOptions = _pieceOptions.removeLast();
221+
var childOptions = _options.removeLast();
249222

250223
// If the child [piece] contains a newline then this one transitively does.
251-
if (childOptions.hasNewline) _handleNewline();
224+
if (childOptions.hasNewline && _options.isNotEmpty) _handleNewline();
252225
}
253226

254227
/// Format [piece] if not null.
@@ -258,30 +231,26 @@ class CodeWriter {
258231

259232
/// Sets [selectionStart] to be [start] code units into the output.
260233
void startSelection(int start) {
261-
assert(_selectionStart == null);
262-
263234
_flushWhitespace();
264-
_selectionStart = _buffer.length + start;
235+
_solution.startSelection(_buffer.length + start);
265236
}
266237

267238
/// Sets [selectionEnd] to be [end] code units into the output.
268239
void endSelection(int end) {
269-
assert(_selectionEnd == null);
270-
271240
_flushWhitespace();
272-
_selectionEnd = _buffer.length + end;
241+
_solution.endSelection(_buffer.length + end);
273242
}
274243

275244
/// Notes that a newline has been written.
276245
///
277246
/// If this occurs in a place where newlines are prohibited, then invalidates
278247
/// the solution.
279248
void _handleNewline() {
280-
if (!_options.allowNewlines) _hasInvalidNewline = true;
249+
if (!_options.last.allowNewlines) _solution.invalidate(_options.last.piece);
281250

282251
// Note that this piece contains a newline so that we can propagate that
283252
// up to containing pieces too.
284-
_options.hasNewline = true;
253+
_options.last.hasNewline = true;
285254
}
286255

287256
/// Write any pending whitespace.
@@ -314,15 +283,15 @@ class CodeWriter {
314283
void _finishLine() {
315284
// If the completed line is too long, track the overflow.
316285
if (_column >= _pageWidth) {
317-
_overflow += _column - _pageWidth;
286+
_solution.addOverflow(_column - _pageWidth);
318287
}
319288

320289
// If we found a problematic line, and there is a piece on the line that
321290
// we can try to split, then remember that piece so that the solution will
322291
// expand it next.
323292
if (!_foundExpandLine &&
324293
_nextPieceToExpand != null &&
325-
(_column > _pageWidth || _hasInvalidNewline)) {
294+
(_column > _pageWidth || !_solution.isValid)) {
326295
// We found a problematic line, so remember it and the piece on it.
327296
_foundExpandLine = true;
328297
} else if (!_foundExpandLine) {
@@ -359,6 +328,9 @@ enum Whitespace {
359328

360329
/// The mutable state local to a single piece being formatted.
361330
class _PieceOptions {
331+
/// The piece being formatted with these options.
332+
final Piece piece;
333+
362334
/// The absolute number of spaces of leading indentation coming from
363335
/// block-like structure or explicit extra indentation (aligning constructor
364336
/// initializers, `show` clauses, etc.).
@@ -373,5 +345,5 @@ class _PieceOptions {
373345
/// Whether any newlines have occurred in this piece or any of its children.
374346
bool hasNewline = false;
375347

376-
_PieceOptions(this.indent, this.allowNewlines);
348+
_PieceOptions(this.piece, this.indent, this.allowNewlines);
377349
}

0 commit comments

Comments
 (0)