blob: 9fa106bdf691f5d18d1cb80da7c303fe6b89adcc [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import '../piece/piece.dart';
import 'code_writer.dart';
/// A single possible set of formatting choices.
///
/// Each solution binds some number of [Piece]s in the piece tree to [State]s.
/// (Any pieces whose states are not bound are treated as having a default
/// unsplit state.)
///
/// Given that set of states, we can create a [CodeWriter] and give that to all
/// of the pieces in the tree so they can format themselves. That in turn
/// yields a total number of overflow characters, cost, and formatted output,
/// which are all stored here.
class Solution implements Comparable<Solution> {
/// The states that pieces have been bound to.
///
/// Note that order that keys are inserted into this map is significant. When
/// ordering solutions, we use the order that pieces are bound in here to
/// break ties between solutions that otherwise have the same cost and
/// overflow.
final Map<Piece, State> _pieceStates;
/// The amount of penalties applied based on the chosen line splits.
final int cost;
/// The formatted code.
String get text => _text;
late final String _text;
/// Whether this score is for a valid solution or not.
///
/// An invalid solution is one where a hard newline appears in a context
/// where splitting isn't allowed. This is considered worse than any other
/// solution.
bool get isValid => _invalidPiece == null;
/// The piece that forbid newlines when an invalid newline was written, or
/// `null` if no invalid newline has occurred.
Piece? _invalidPiece;
/// The total number of characters that do not fit inside the page width.
int get overflow => _overflow;
int _overflow = 0;
/// The unsolved piece in this solution that should be expanded next to
/// produce new more refined solutions, if there is one.
///
/// The tree of possible solutions is combinatorial in the number of pieces
/// and exponential in the number of states those pieces can take. We can't
/// afford to brute force explore the whole tree, even with the optimization
/// that we stop as soon as we find a solution with no overflow.
///
/// Most possible solutions add unnecessary splits in regions of the code
/// that already fit within the page width. Exploring those is wasted time.
/// To avoid that, we rely on a couple of insights:
///
/// First, the solver treats any piece with an unselected state as being
/// unsplit. This means that refining a solution always takes a piece that is
/// unsplit and makes it split more. That monotonically increases the cost,
/// but may help fit the solution inside the page.
///
/// Therefore, we don't want to select states for most pieces. Only pieces
/// that need to split in order to find a solution that fits in the page
/// width or that are necessary because the unsplit state is invalid. (The
/// latter usually means a line comment or statement occurs inside the piece.)
///
/// So we skip past any pieces that aren't on overflowing lines or on lines
/// whose newline led to an invalid solution. Further, it's also the case
/// that splitting an earlier pieces will often reshuffle the formatting of
/// much of the code following it.
///
/// Thus we only worry about the *first* unsolved piece on the first
/// problematic line when expanding. If selecting states for that piece still
/// doesn't help, the solver will work its way through later pieces from those
/// subsequenct partial solutions.
///
/// This lets us efficiently skip through almost all of the pieces that don't
/// need to be touched in order to find a valid solution.
///
/// If this is `null`, then there are no further solutions to generate from
/// this one. It's either a dead end or a winner.
late final Piece? _nextPieceToExpand;
/// The offset in [text] where the selection starts, or `null` if there is
/// no selection.
int? get selectionStart => _selectionStart;
int? _selectionStart;
/// The offset in [text] where the selection ends, or `null` if there is
/// no selection.
int? get selectionEnd => _selectionEnd;
int? _selectionEnd;
/// Creates a new [Solution] with no pieces set to any state (which
/// implicitly means they have state [State.unsplit] unless they're pinned to
/// another state).
factory Solution(Piece root, int pageWidth) {
var pieceStates = <Piece, State>{};
var cost = 0;
// Bind every pinned piece to its state and propagate any constraints from
// those.
void traversePinned(Piece piece) {
if (piece.pinnedState case var pinned?) {
var additionalCost = _tryBind(pieceStates, piece, pinned);
// Pieces should be implemented such that they never get pinned into a
// conflicting state because then there's no possible solution, so
// [_tryBind()] should always succeed and [additionalCost] won't be
// `null`.
cost += additionalCost!;
}
piece.forEachChild(traversePinned);
}
traversePinned(root);
return Solution._(root, pageWidth, cost, pieceStates);
}
Solution._(Piece root, int pageWidth, this.cost, this._pieceStates) {
var writer = CodeWriter(pageWidth, this);
writer.format(root);
var (text, nextPieceToExpand) = writer.finish();
_text = text;
_nextPieceToExpand = nextPieceToExpand;
}
/// The state this solution selects for [piece].
///
/// If no state has been selected, defaults to the first state.
State pieceState(Piece piece) => _pieceStates[piece] ?? State.unsplit;
/// Whether [piece] has been bound to a state in this set.
bool isBound(Piece piece) => _pieceStates.containsKey(piece);
/// Increases the total overflow for this solution by [overflow].
///
/// This should only be called by [CodeWriter].
void addOverflow(int overflow) {
_overflow += overflow;
}
/// Sets [selectionStart] to be [start] code units into the output.
///
/// This should only be called by [CodeWriter].
void startSelection(int start) {
assert(_selectionStart == null);
_selectionStart = start;
}
/// Sets [selectionEnd] to be [end] code units into the output.
///
/// This should only be called by [CodeWriter].
void endSelection(int end) {
assert(_selectionEnd == null);
_selectionEnd = end;
}
/// Mark this solution as having a newline where none is permitted by [piece]
/// and is thus not a valid solution.
///
/// This should only be called by [CodeWriter].
void invalidate(Piece piece) {
_invalidPiece = piece;
}
/// Derives new potential solutions from this one by binding
/// [_nextPieceToExpand] to all of its possible states.
///
/// If there is no potential piece to expand, or all attempts to expand it
/// fail, returns an empty list.
List<Solution> expand(Piece root, int pageWidth) {
// If the piece whose newline constraint was violated is already bound to
// one state, then every solution derived from this one will also fail in
// the same way, so discard the whole solution tree hanging off this one.
if (_invalidPiece case var piece? when isBound(piece)) return const [];
var expandPiece = _nextPieceToExpand;
// If there is no piece that we can expand on this solution, it's a dead
// end (or a winner).
if (expandPiece == null) return const [];
// TODO(perf): If `_invalidPiece == expandPiece`, then we know that the
// first state leads to an invalid solution, so there's no point in trying
// to expand to a solution that binds `expandPiece` to
// `expandPiece.states[0]`. We should be able to do:
//
// Iterable<State> states = expandPiece.states;
// if (_invalidPiece == expandPiece) {
// print('skip $expandPiece ${states.first}');
// states = states.skip(1);
// }
//
// And then use `states` below. But when I tried that, it didn't seem to
// make any noticeable performance difference on the one pathological
// example I tried. Leaving this here as a TODO to investigate more when
// there are other benchmarks we can try.
var solutions = <Solution>[];
// For each state that the expanding piece can be in, create a new solution
// that inherits all of the bindings of this one, and binds the expanding
// piece to that state (along with any further pieces constrained by that
// one).
for (var state in expandPiece.states) {
var newStates = {..._pieceStates};
var additionalCost = _tryBind(newStates, expandPiece, state);
// Discard the solution if we hit a constraint violation.
if (additionalCost == null) continue;
solutions
.add(Solution._(root, pageWidth, cost + additionalCost, newStates));
}
return solutions;
}
/// Compares two solutions where a more desirable solution comes first.
///
/// For performance, we want to stop checking solutions as soon as we find
/// the best one. Best means the fewest overflow characters and the lowest
/// code.
@override
int compareTo(Solution other) {
// Even though overflow is "worse" than cost, we order in terms of cost
// because a solution with overflow may lead to a low-cost solution without
// overflow, so we want to explore in cost order.
if (cost != other.cost) return cost.compareTo(other.cost);
if (overflow != other.overflow) return overflow.compareTo(other.overflow);
// If all else is equal, prefer lower states in earlier bound pieces.
for (var piece in _pieceStates.keys) {
var thisState = pieceState(piece);
var otherState = other.pieceState(piece);
if (thisState != otherState) return thisState.compareTo(otherState);
}
return 0;
}
@override
String toString() {
var states = _pieceStates.keys
.map((piece) => '$piece:${_pieceStates[piece]}')
.join(' ');
return [
'\$$cost',
if (overflow > 0) '($overflow over)',
if (!isValid) '(invalid)',
states,
].join(' ');
}
/// Attempts to add a binding from [piece] to [state] in [boundStates], and
/// then adds any further bindings from constraints that [piece] applies to
/// its children, recursively.
///
/// This may fail if [piece] is already bound to a different [state], or if
/// any constrained pieces are bound to different states.
///
/// If successful, returns the additional cost required to bind [piece] to
/// [state] (along with any other applied constrained pieces). Otherwise,
/// returns `null` to indicate failure.
static int? _tryBind(
Map<Piece, State> boundStates, Piece piece, State state) {
var success = true;
var additionalCost = 0;
void bind(Piece thisPiece, State thisState) {
// If we've already failed from a previous sibling's constraint violation,
// early out.
if (!success) return;
// Apply the new binding if it doesn't conflict with an existing one.
switch (boundStates[thisPiece]) {
case null:
// Binding a unbound piece to a state.
additionalCost += thisPiece.stateCost(thisState);
boundStates[thisPiece] = thisState;
// This piece may in turn place further constraints on others.
thisPiece.applyConstraints(thisState, bind);
case var alreadyBound when alreadyBound != thisState:
// Already bound to a different state, so there's a conflict.
success = false;
default:
break; // Already bound to the same state, so nothing to do.
}
}
bind(piece, state);
return success ? additionalCost : null;
}
}