blob: 69cba90ff3bc4cc32c811059c0dc50306238d1f6 [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 '../profile.dart';
import 'code_writer.dart';
import 'solution_cache.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 set of states that pieces are allowed to be in without violating
/// constraints of already bound pieces.
///
/// Each key is a constrained piece and the values are the remaining states
/// that the piece may take which aren't known to violate existing
/// constraints. If a piece is not in this map, then there are no constraints
/// on it.
final Map<Piece, List<State>> _allowedStates;
/// The amount of penalties applied based on the chosen line splits.
int get cost => _cost;
int _cost;
/// The formatted code.
String get text => _text;
late final String _text;
/// False if this Solution contains a newline where one is prohibited.
///
/// An invalid solution may have no overflow characters and the lowest score,
/// but is still considered worse than any other valid solution.
bool get isValid => _isValid;
bool _isValid = true;
/// Whether the solution contains an invalid newline and the piece that
/// prohibits the newline is bound in this solution.
///
/// When this is `true`, it means this solution and every solution that could
/// be derived from it is invalid so the whole solution tree can be discarded.
bool _isDeadEnd = false;
/// 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 earlier pieces will often reshuffle the formatting of much
/// of the code following it.
///
/// Thus we only worry about unsolved pieces on the *first* problematic line
/// when expanding. If selecting states for those pieces still doesn't help,
/// the solver will work its way through later pieces from those subsequent
/// 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 empty, then there are no further solutions to generate from
/// this one. It's either a dead end or a winner.
late final List<Piece> _expandPieces;
/// 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(SolutionCache cache, Piece root,
{required int pageWidth, required int leadingIndent, State? rootState}) {
var solution =
Solution._(cache, root, pageWidth, leadingIndent, 0, {}, {}, rootState);
solution._format(cache, root, pageWidth, leadingIndent);
return solution;
}
Solution._(SolutionCache cache, Piece root, int pageWidth, int leadingIndent,
this._cost, this._pieceStates, this._allowedStates,
[State? rootState]) {
Profile.count('create Solution');
// If we're formatting a subtree of a larger Piece tree that binds [root]
// to [rootState], then bind it in this solution too.
if (rootState != null) _bind(root, rootState);
}
/// Attempt to eagerly bind [piece] to a state given that it must fit within
/// [pageWidth] (which is the overall page width minus any leading indentation
/// in the solution where this is called).
///
/// If it can, binds the piece to that state in this solution and returns
/// `true`. Otherwise returns `false`.
bool tryBindByPageWidth(Piece piece, int pageWidth) {
if (piece.fixedStateForPageWidth(pageWidth) case var state?) {
_bind(piece, state);
return true;
}
return false;
}
/// The state that [piece] is pinned to or that this solution selects.
///
/// If no state has been selected, defaults to the first state.
State pieceState(Piece piece) => pieceStateIfBound(piece) ?? State.unsplit;
/// The state that [piece] is pinned to or that this solution selects.
///
/// If no state has been selected, returns `null`.
State? pieceStateIfBound(Piece piece) =>
piece.pinnedState ?? _pieceStates[piece];
/// Whether [piece] has been bound to a state in this set (or is pinned).
bool isBound(Piece piece) =>
piece.pinnedState != null || _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;
}
/// Apply the overflow, cost, and bound states from [subtreeSolution] to this
/// solution.
///
/// This is called when a subtree of a Piece tree is solved separately and
/// the resulting solution is being merged with this one.
void mergeSubtree(Solution subtreeSolution) {
Profile.begin('Solution.mergeSubtree()');
_overflow += subtreeSolution._overflow;
// Add the subtree's bound pieces to this one. Make sure to not double
// count costs for pieces that are already bound in this one.
subtreeSolution._pieceStates.forEach((piece, state) {
_pieceStates.putIfAbsent(piece, () {
_cost += piece.stateCost(state);
return state;
});
});
Profile.end('Solution.mergeSubtree()');
}
/// 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) {
_isValid = false;
// If the piece whose newline constraint was violated is already bound to
// one state, then every solution derived from this one will also fail.
if (!_isDeadEnd && isBound(piece)) _isDeadEnd = true;
}
/// Derives new potential solutions from this one by binding [_expandPieces]
/// to all of their possible states.
///
/// If there is no potential piece to expand, or all attempts to expand it
/// fail, returns an empty list.
List<Solution> expand(SolutionCache cache, Piece root,
{required int pageWidth, required int leadingIndent}) {
// If there is no piece that we can expand on this solution, it's a dead
// end (or a winner).
if (_expandPieces.isEmpty) return const [];
var solutions = <Solution>[];
for (var i = 0; i < _expandPieces.length; i++) {
// For each non-default 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).
var expandPiece = _expandPieces[i];
for (var state
in _allowedStates[expandPiece] ?? expandPiece.additionalStates) {
var expanded = Solution._(cache, root, pageWidth, leadingIndent, cost,
{..._pieceStates}, {..._allowedStates});
// Bind all preceding expand pieces to their unsplit state. Their
// other states have already been expanded by earlier iterations of
// the outer for loop.
var valid = true;
for (var j = 0; j < i; j++) {
expanded._bind(_expandPieces[j], State.unsplit);
if (expanded._isDeadEnd) {
valid = false;
break;
}
}
// Discard the solution if we hit a constraint violation.
if (!valid) continue;
expanded._bind(expandPiece, state);
// Discard the solution if we hit a constraint violation.
if (!expanded._isDeadEnd) {
expanded._format(cache, root, pageWidth, leadingIndent);
// TODO(rnystrom): These come mostly (entirely?) from hard newlines
// in sequences, comments, and multiline strings. It should be
// possible to handle those during piece construction too. If we do,
// remove this check.
// We may not detect some newline violations until formatting.
if (!expanded._isDeadEnd) solutions.add(expanded);
}
}
}
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 = [
for (var MapEntry(key: piece, value: state) in _pieceStates.entries)
if (piece.additionalStates.isNotEmpty && piece.pinnedState == null)
'$piece$state'
];
return [
'\$$cost',
if (overflow > 0) '($overflow over)',
if (!isValid) '(invalid)',
states.join(' '),
].join(' ').trim();
}
/// Run a [CodeWriter] on this solution to produce the final formatted output
/// and calculate the overflow and expand pieces.
void _format(
SolutionCache cache, Piece root, int pageWidth, int leadingIndent) {
var writer = CodeWriter(pageWidth, leadingIndent, cache, this);
writer.format(root);
var (text, expandPieces) = writer.finish();
_text = text;
_expandPieces = expandPieces;
}
/// Attempts to add a binding from [piece] to [state] to the solution, and
/// then adds any further bindings from constraints that [piece] applies to
/// its children, recursively.
///
/// This may invalidate the solution if [piece] is already bound to a
/// different [state], or if any constrained pieces are bound to different
/// states.
///
/// If successful, adds the cost required to bind [piece] to [state] (along
/// with any other applied constrained pieces). Otherwise, marks the solution
/// as a dead end.
void _bind(Piece piece, State state) {
// If we've already failed from a previous violation, early out.
if (_isDeadEnd) return;
// Apply the new binding if it doesn't conflict with an existing one.
switch (pieceStateIfBound(piece)) {
case null:
// Binding a unbound piece to a state.
_cost += piece.stateCost(state);
_pieceStates[piece] = state;
// This piece may in turn place further constraints on others.
piece.applyConstraints(state, _bind);
// If this piece's state prevents some of its children from having
// newlines, then further constrain those children.
if (!_isDeadEnd) {
piece.forEachChild((child) {
// Stop as soon as we fail.
if (_isDeadEnd) return;
// If the child can have newlines, there is no constraint.
if (piece.allowNewlineInChild(state, child)) return;
// Otherwise, don't let any piece under [child] contain newlines.
_constrainOffspring(child);
});
}
case var alreadyBound when alreadyBound != state:
// Already bound to a different state, so there's a conflict.
_isDeadEnd = true;
_isValid = false;
default:
break; // Already bound to the same state, so nothing to do.
}
}
/// For [piece] and its transitive offspring subtree, eliminate any state that
/// will always produce a newline since that state is not permitted because
/// the parent of [piece] doesn't allow [piece] to have any newlines.
void _constrainOffspring(Piece piece) {
for (var offspring in piece.statefulOffspring) {
if (_isDeadEnd) break;
if (pieceStateIfBound(offspring) case var boundState?) {
// This offspring is already pinned or bound to a state. If that
// state will emit newlines, then this solution is invalid.
if (offspring.containsNewline(boundState)) {
_isDeadEnd = true;
_isValid = false;
}
} else if (!_allowedStates.containsKey(offspring)) {
// If we get here, the offspring isn't bound to a state and we haven't
// already constrained it. Eliminate any of its states that will emit
// newlines.
var allowedUnsplit = !offspring.containsNewline(State.unsplit);
var states = offspring.additionalStates;
var remainingStates = <State>[];
for (var state in states) {
if (!offspring.containsNewline(state)) {
remainingStates.add(state);
}
}
if (!allowedUnsplit && remainingStates.isEmpty) {
// There is no state this child can take that won't emit newlines,
// and it's not allowed to, so this solution is bad.
_isDeadEnd = true;
_isValid = false;
} else if (remainingStates.isEmpty) {
// The only valid state is unsplit so bind it to that.
_bind(offspring, State.unsplit);
} else if (!allowedUnsplit && remainingStates.length == 1) {
// There's only one valid state, so bind it to that.
_bind(offspring, remainingStates.first);
} else if (remainingStates.length < states.length) {
// There are some constrained states, so keep the remaining ones.
_allowedStates[offspring] = remainingStates;
}
}
}
}
}