Index: dart_style/lib/src/line_splitting/solve_state.dart |
diff --git a/dart_style/lib/src/line_splitting/solve_state.dart b/dart_style/lib/src/line_splitting/solve_state.dart |
deleted file mode 100644 |
index b4197561aa9fc9912e3018ef00391b0d09e9c78c..0000000000000000000000000000000000000000 |
--- a/dart_style/lib/src/line_splitting/solve_state.dart |
+++ /dev/null |
@@ -1,467 +0,0 @@ |
-// Copyright (c) 2015, 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. |
- |
-library dart_style.src.line_splitting.solve_state; |
- |
-import '../debug.dart' as debug; |
-import '../rule/rule.dart'; |
-import 'line_splitter.dart'; |
-import 'rule_set.dart'; |
- |
-/// A possibly incomplete solution in the line splitting search space. |
-/// |
-/// A single [SolveState] binds some subset of the rules to values while |
-/// leaving the rest unbound. If every rule is bound, the solve state describes |
-/// a complete solution to the line splitting problem. Even if rules are |
-/// unbound, a state can also usually be used as a solution by treating all |
-/// unbound rules as unsplit. (The usually is because a state that constrains |
-/// an unbound rule to split can't be used with that rule unsplit.) |
-/// |
-/// From a given solve state, we can explore the search tree to more refined |
-/// solve states by producing new ones that add more bound rules to the current |
-/// state. |
-class SolveState { |
- final LineSplitter _splitter; |
- final RuleSet _ruleValues; |
- |
- /// The unbound rules in this state that can be bound to produce new more |
- /// refined states. |
- /// |
- /// Keeping this set small is the key to make the entire line splitter |
- /// perform well. If we consider too make rules at each state, our |
- /// exploration of the solution space is too branchy and we waste time on |
- /// dead end solutions. |
- /// |
- /// Here is the key insight. The line splitter treats any unbound rule as |
- /// being unsplit. This means refining a solution always means taking a rule |
- /// that is unsplit and making it split. That monotonically increases the |
- /// cost, but may help fit the solution inside the page. |
- /// |
- /// We want to keep the cost low, so the only reason to consider making a |
- /// rule split is if it reduces an overflowing line. It's also the case that |
- /// splitting an earlier rule will often reshuffle the rest of the line. |
- /// |
- /// Taking that into account, the only rules we consider binding to extend a |
- /// solve state are *unbound rules inside the first line that is overflowing*. |
- /// Even if a line has dozens of rules, this generally keeps the branching |
- /// down to a few. It also means rules inside lines that already fit are |
- /// never touched. |
- /// |
- /// There is one other set of rules that go in here. Sometimes a bound rule |
- /// in the solve state constrains some other unbound rule to split. In that |
- /// case, we also consider that active so we know to not leave it at zero. |
- final _liveRules = new Set<Rule>(); |
- |
- /// The set of splits chosen for this state. |
- SplitSet get splits => _splits; |
- SplitSet _splits; |
- |
- /// The number of characters that do not fit inside the page with this set of |
- /// splits. |
- int get overflowChars => _overflowChars; |
- int _overflowChars; |
- |
- /// Whether we can treat this state as a complete solution by leaving its |
- /// unbound rules unsplit. |
- /// |
- /// This is generally true but will be false if the state contains any |
- /// unbound rules that are constrained to not be zero by other bound rules. |
- /// This avoids picking a solution that leaves those rules at zero when they |
- /// aren't allowed to be. |
- bool _isComplete = true; |
- |
- /// The constraints the bound rules in this state have on the remaining |
- /// unbound rules. |
- Map<Rule, int> _constraints; |
- |
- /// The bound rules that appear inside lines also containing unbound rules. |
- /// |
- /// By appearing in the same line, it means these bound rules may affect the |
- /// results of binding those unbound rules. This is used to tell if two |
- /// states may diverge by binding unbound rules or not. |
- Set<Rule> _boundRulesInUnboundLines; |
- |
- SolveState(this._splitter, this._ruleValues) { |
- _calculateSplits(); |
- _calculateCost(); |
- } |
- |
- /// Gets the value to use for [rule], either the bound value or `0` if it |
- /// isn't bound. |
- int getValue(Rule rule) { |
- if (rule is HardSplitRule) return 0; |
- |
- return _ruleValues.getValue(rule); |
- } |
- |
- /// Returns `true` if this state is a better solution to use as the final |
- /// result than [other]. |
- bool isBetterThan(SolveState other) { |
- // If this state contains an unbound rule that we know can't be left |
- // unsplit, we can't pick this as a solution. |
- if (!_isComplete) return false; |
- |
- // Anything is better than nothing. |
- if (other == null) return true; |
- |
- // Prefer the solution that fits the most in the page. |
- if (overflowChars != other.overflowChars) { |
- return overflowChars < other.overflowChars; |
- } |
- |
- // Otherwise, prefer the best cost. |
- return splits.cost < other.splits.cost; |
- } |
- |
- /// Determines if this state "overlaps" [other]. |
- /// |
- /// Two states overlap if they currently have the same score and we can tell |
- /// for certain that they won't diverge as their unbound rules are bound. If |
- /// that's the case, then whichever state is better now (based on their |
- /// currently bound rule values) is the one that will always win, regardless |
- /// of how they get expanded. |
- /// |
- /// In other words, their entire expanded solution trees also overlap. In |
- /// that case, there's no point in expanding both, so we can just pick the |
- /// winner now and discard the other. |
- /// |
- /// For this to be true, we need to prove that binding an unbound rule won't |
- /// affect one state differently from the other. We have to show that they |
- /// are parallel. |
- /// |
- /// Two things could cause this *not* to be the case. |
- /// |
- /// 1. If one state's bound rules place different constraints on the unbound |
- /// rules than the other. |
- /// |
- /// 2. If one state's different bound rules are in the same line as an |
- /// unbound rule. That affects the indentation and length of the line, |
- /// which affects the context where the unbound rule is being chosen. |
- /// |
- /// If neither of these is the case, the states overlap. Returns `<0` if this |
- /// state is better, or `>0` if [other] wins. If the states do not overlap, |
- /// returns `0`. |
- int compareOverlap(SolveState other) { |
- if (!_isOverlapping(other)) return 0; |
- |
- // They do overlap, so see which one wins. |
- for (var rule in _splitter.rules) { |
- var value = _ruleValues.getValue(rule); |
- var otherValue = other._ruleValues.getValue(rule); |
- |
- if (value != otherValue) return value.compareTo(otherValue); |
- } |
- |
- // The way SolveStates are expanded should guarantee that we never generate |
- // the exact same state twice. Getting here implies that that failed. |
- throw "unreachable"; |
- } |
- |
- /// Enqueues more solve states to consider based on this one. |
- /// |
- /// For each unbound rule in this state that occurred in the first long line, |
- /// enqueue solve states that bind that rule to each value it can have and |
- /// bind all previous rules to zero. (In other words, try all subsolutions |
- /// where that rule becomes the first new rule to split at.) |
- void expand() { |
- var unsplitRules = _ruleValues.clone(); |
- |
- // Walk down the rules looking for unbound ones to try. |
- var triedRules = 0; |
- for (var rule in _splitter.rules) { |
- if (_liveRules.contains(rule)) { |
- // We found one worth trying, so try all of its values. |
- for (var value = 1; value < rule.numValues; value++) { |
- var boundRules = unsplitRules.clone(); |
- |
- var mustSplitRules; |
- var valid = boundRules.tryBind(_splitter.rules, rule, value, (rule) { |
- if (mustSplitRules == null) mustSplitRules = []; |
- mustSplitRules.add(rule); |
- }); |
- |
- // Make sure we don't violate the constraints of the bound rules. |
- if (!valid) continue; |
- |
- var state = new SolveState(_splitter, boundRules); |
- |
- // If some unbound rules are constrained to split, remember that. |
- if (mustSplitRules != null) { |
- state._isComplete = false; |
- state._liveRules.addAll(mustSplitRules); |
- } |
- |
- _splitter.enqueue(state); |
- } |
- |
- // Stop once we've tried all of the ones we can. |
- if (++triedRules == _liveRules.length) break; |
- } |
- |
- // Fill in previous unbound rules with zero. |
- if (!_ruleValues.contains(rule)) { |
- // Pass a dummy callback because zero will never fail. (If it would |
- // have, that rule would already be bound to some other value.) |
- if (!unsplitRules.tryBind(_splitter.rules, rule, 0, (_) {})) { |
- break; |
- } |
- } |
- } |
- } |
- |
- /// Returns `true` if [other] overlaps this state. |
- bool _isOverlapping(SolveState other) { |
- _ensureOverlapFields(); |
- other._ensureOverlapFields(); |
- |
- // Lines that contain both bound and unbound rules must have the same |
- // bound values. |
- if (_boundRulesInUnboundLines.length != |
- other._boundRulesInUnboundLines.length) { |
- return false; |
- } |
- |
- for (var rule in _boundRulesInUnboundLines) { |
- if (!other._boundRulesInUnboundLines.contains(rule)) return false; |
- if (_ruleValues.getValue(rule) != other._ruleValues.getValue(rule)) { |
- return false; |
- } |
- } |
- |
- if (_constraints.length != other._constraints.length) return false; |
- |
- for (var rule in _constraints.keys) { |
- if (_constraints[rule] != other._constraints[rule]) return false; |
- } |
- |
- return true; |
- } |
- |
- /// Calculates the [SplitSet] for this solve state, assuming any unbound |
- /// rules are set to zero. |
- void _calculateSplits() { |
- // Figure out which expression nesting levels got split and need to be |
- // assigned columns. |
- var usedNestingLevels = new Set(); |
- for (var i = 0; i < _splitter.chunks.length - 1; i++) { |
- var chunk = _splitter.chunks[i]; |
- if (chunk.rule.isSplit(getValue(chunk.rule), chunk)) { |
- usedNestingLevels.add(chunk.nesting); |
- chunk.nesting.clearTotalUsedIndent(); |
- } |
- } |
- |
- for (var nesting in usedNestingLevels) { |
- nesting.refreshTotalUsedIndent(usedNestingLevels); |
- } |
- |
- _splits = new SplitSet(_splitter.chunks.length); |
- for (var i = 0; i < _splitter.chunks.length - 1; i++) { |
- var chunk = _splitter.chunks[i]; |
- if (chunk.rule.isSplit(getValue(chunk.rule), chunk)) { |
- var indent = 0; |
- if (!chunk.flushLeftAfter) { |
- // Add in the chunk's indent. |
- indent = _splitter.blockIndentation + chunk.indent; |
- |
- // And any expression nesting. |
- indent += chunk.nesting.totalUsedIndent; |
- } |
- |
- _splits.add(i, indent); |
- } |
- } |
- } |
- |
- /// Evaluates the cost (i.e. the relative "badness") of splitting the line |
- /// into [lines] physical lines based on the current set of rules. |
- void _calculateCost() { |
- assert(_splits != null); |
- |
- // Calculate the length of each line and apply the cost of any spans that |
- // get split. |
- var cost = 0; |
- _overflowChars = 0; |
- |
- var length = _splitter.firstLineIndent; |
- |
- // The unbound rules in use by the current line. This will be null after |
- // the first long line has completed. |
- var currentLineRules = []; |
- |
- endLine(int end) { |
- // Track lines that went over the length. It is only rules contained in |
- // long lines that we may want to split. |
- if (length > _splitter.writer.pageWidth) { |
- _overflowChars += length - _splitter.writer.pageWidth; |
- |
- // Only try rules that are in the first long line, since we know at |
- // least one of them *will* be split. |
- if (currentLineRules != null && currentLineRules.isNotEmpty) { |
- _liveRules.addAll(currentLineRules); |
- currentLineRules = null; |
- } |
- } else { |
- // The line fit, so don't keep track of its rules. |
- if (currentLineRules != null) { |
- currentLineRules.clear(); |
- } |
- } |
- } |
- |
- // The set of spans that contain chunks that ended up splitting. We store |
- // these in a set so a span's cost doesn't get double-counted if more than |
- // one split occurs in it. |
- var splitSpans = new Set(); |
- |
- for (var i = 0; i < _splitter.chunks.length; i++) { |
- var chunk = _splitter.chunks[i]; |
- |
- length += chunk.text.length; |
- |
- // Ignore the split after the last chunk. |
- if (i == _splitter.chunks.length - 1) break; |
- |
- if (_splits.shouldSplitAt(i)) { |
- endLine(i); |
- |
- splitSpans.addAll(chunk.spans); |
- |
- // Include the cost of the nested block. |
- if (chunk.blockChunks.isNotEmpty) { |
- cost += |
- _splitter.writer.formatBlock(chunk, _splits.getColumn(i)).cost; |
- } |
- |
- // Start the new line. |
- length = _splits.getColumn(i); |
- } else { |
- if (chunk.spaceWhenUnsplit) length++; |
- |
- // Include the nested block inline, if any. |
- length += chunk.unsplitBlockLength; |
- |
- // If we might be in the first overly long line, keep track of any |
- // unbound rules we encounter. These are ones that we'll want to try to |
- // bind to shorten the long line. |
- if (currentLineRules != null && |
- chunk.rule != null && |
- !chunk.isHardSplit && |
- !_ruleValues.contains(chunk.rule)) { |
- currentLineRules.add(chunk.rule); |
- } |
- } |
- } |
- |
- // Add the costs for the rules that split. |
- _ruleValues.forEach(_splitter.rules, (rule, value) { |
- // A rule may be bound to zero if another rule constrains it to not split. |
- if (value != 0) cost += rule.cost; |
- }); |
- |
- // Add the costs for the spans containing splits. |
- for (var span in splitSpans) cost += span.cost; |
- |
- // Finish the last line. |
- endLine(_splitter.chunks.length); |
- |
- _splits.setCost(cost); |
- } |
- |
- /// Lazily initializes the fields needed to compare two states for overlap. |
- /// |
- /// We do this lazily because the calculation is a bit slow, and is only |
- /// needed when we have two states with the same score. |
- void _ensureOverlapFields() { |
- if (_constraints != null) return; |
- |
- _calculateConstraints(); |
- _calculateBoundRulesInUnboundLines(); |
- } |
- |
- /// Initializes [_constraints] with any constraints the bound rules place on |
- /// the unbound rules. |
- void _calculateConstraints() { |
- _constraints = {}; |
- |
- var unboundRules = []; |
- var boundRules = []; |
- |
- for (var rule in _splitter.rules) { |
- if (_ruleValues.contains(rule)) { |
- boundRules.add(rule); |
- } else { |
- unboundRules.add(rule); |
- } |
- } |
- |
- for (var bound in boundRules) { |
- var value = _ruleValues.getValue(bound); |
- |
- for (var unbound in unboundRules) { |
- var constraint = bound.constrain(value, unbound); |
- if (constraint != null) { |
- _constraints[unbound] = constraint; |
- } |
- } |
- } |
- } |
- |
- void _calculateBoundRulesInUnboundLines() { |
- _boundRulesInUnboundLines = new Set(); |
- |
- var boundInLine = new Set(); |
- var hasUnbound = false; |
- |
- for (var i = 0; i < _splitter.chunks.length - 1; i++) { |
- if (splits.shouldSplitAt(i)) { |
- if (hasUnbound) _boundRulesInUnboundLines.addAll(boundInLine); |
- |
- boundInLine.clear(); |
- hasUnbound = false; |
- } |
- |
- var rule = _splitter.chunks[i].rule; |
- if (rule != null && rule is! HardSplitRule) { |
- if (_ruleValues.contains(rule)) { |
- boundInLine.add(rule); |
- } else { |
- hasUnbound = true; |
- } |
- } |
- } |
- |
- if (hasUnbound) _boundRulesInUnboundLines.addAll(boundInLine); |
- } |
- |
- String toString() { |
- var buffer = new StringBuffer(); |
- |
- buffer.writeAll(_splitter.rules.map((rule) { |
- var valueLength = "${rule.fullySplitValue}".length; |
- |
- var value = "?"; |
- if (_ruleValues.contains(rule)) { |
- value = "${_ruleValues.getValue(rule)}"; |
- } |
- |
- value = value.padLeft(valueLength); |
- if (_liveRules.contains(rule)) { |
- value = debug.bold(value); |
- } else { |
- value = debug.gray(value); |
- } |
- |
- return value; |
- }), " "); |
- |
- buffer.write(" \$${splits.cost}"); |
- |
- if (overflowChars > 0) buffer.write(" (${overflowChars} over)"); |
- if (!_isComplete) buffer.write(" (incomplete)"); |
- if (splits == null) buffer.write(" invalid"); |
- |
- return buffer.toString(); |
- } |
-} |