Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(722)

Side by Side Diff: lib/src/line_splitting/solve_state.dart

Issue 1257903002: Optimize splitting long, complex lines. (Closed) Base URL: https://github.com/dart-lang/dart_style.git@master
Patch Set: Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « lib/src/line_splitting/rule_set.dart ('k') | lib/src/line_splitting/solve_state_queue.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library dart_style.src.line_splitter; 5 library dart_style.src.line_splitting.solve_state;
6 6
7 import 'package:collection/priority_queue.dart'; 7 import '../debug.dart' as debug;
8 8 import '../rule/rule.dart';
9 import 'chunk.dart'; 9 import 'line_splitter.dart';
10 import 'debug.dart' as debug;
11 import 'line_writer.dart';
12 import 'rule/rule.dart';
13 import 'rule_set.dart'; 10 import 'rule_set.dart';
14 11
15 /// To ensure the solver doesn't go totally pathological on giant code, we cap
16 /// it at a fixed number of attempts.
17 ///
18 /// If the optimal solution isn't found after this many tries, it just uses the
19 /// best it found so far.
20 const _maxAttempts = 50000;
21
22 /// Takes a set of chunks and determines the best values for its rules in order
23 /// to fit it inside the page boundary.
24 ///
25 /// This problem is exponential in the number of rules and a single expression
26 /// in Dart can be quite large, so it isn't feasible to brute force this. For
27 /// example:
28 ///
29 /// outer(
30 /// fn(1 + 2, 3 + 4, 5 + 6, 7 + 8),
31 /// fn(1 + 2, 3 + 4, 5 + 6, 7 + 8),
32 /// fn(1 + 2, 3 + 4, 5 + 6, 7 + 8),
33 /// fn(1 + 2, 3 + 4, 5 + 6, 7 + 8));
34 ///
35 /// There are 509,607,936 ways this can be split.
36 ///
37 /// The problem is even harder because we may not be able to easily tell if a
38 /// given solution is the best one. It's possible that there is *no* solution
39 /// that fits in the page (due to long strings or identifiers) so the winning
40 /// solution may still have overflow characters. This makes it hard to know
41 /// when we are done and can stop looking.
42 ///
43 /// There are a couple of pieces of domain knowledge we use to cope with this:
44 ///
45 /// - Changing a rule from unsplit to split will never lower its cost. A
46 /// solution with all rules unsplit will always be the one with the lowest
47 /// cost (zero). Conversely, setting all of its rules to the maximum split
48 /// value will always have the highest cost.
49 ///
50 /// (You might think there is a converse rule about overflow characters. The
51 /// solution with the fewest splits will have the most overflow, and the
52 /// solution with the most splits will have the least overflow. Alas, because
53 /// of indentation, that isn't always the case. Adding a split may *increase*
54 /// overflow in some cases.)
55 ///
56 /// - If all of the chunks for a rule are inside lines that already fit in the
57 /// page, then splitting that rule will never improve the solution.
58 ///
59 /// We start off with a [SolveState] where all rules are unbound (which
60 /// implicitly treats them as unsplit). For a given solve state, we can produce
61 /// a set of expanded states that takes some of the rules in the first long
62 /// line and bind them to split values. This always produces new solve states
63 /// with higher cost (but often fewer overflow characters) than the parent
64 /// state.
65 ///
66 /// We take these expanded states and add them to a work list sorted by cost.
67 /// Since unsplit rules always have lower cost solutions, we know that no state
68 /// we enqueue later will ever have a lower cost than the ones we already have
69 /// enqueued.
70 ///
71 /// Then we keep pulling states off the work list and expanding them and adding
72 /// the results back into the list. We do this until we hit a solution where
73 /// all characters fit in the page. The first one we find will have the lowest
74 /// cost and we're done.
75 ///
76 /// We also keep running track of the best solution we've found so far that
77 /// has the fewest overflow characters and the lowest cost. If no solution fits,
78 /// we'll use this one.
79 ///
80 /// As a final escape hatch for pathologically nasty code, after trying some
81 /// fixed maximum number of solve states, we just bail and return the best
82 /// solution found so far.
83 ///
84 /// Even with the above algorithmic optimizations, complex code may still
85 /// require a lot of exploring to find an optimal solution. To make that fast,
86 /// this code is carefully profiled and optimized. If you modify this, make
87 /// sure to test against the benchmark to ensure you don't regress performance.
88 class LineSplitter {
89 final LineWriter _writer;
90
91 /// The list of chunks being split.
92 final List<Chunk> _chunks;
93
94 /// The set of soft rules whose values are being selected.
95 final List<Rule> _rules;
96
97 /// The number of characters of additional indentation to apply to each line.
98 ///
99 /// This is used when formatting blocks to get the output into the right
100 /// column based on where the block appears.
101 final int _blockIndentation;
102
103 /// The starting column of the first line.
104 final int _firstLineIndent;
105
106 /// The list of solve states to explore further.
107 ///
108 /// This is sorted lowest-cost first. This ensures that as soon as we find a
109 /// solution that fits in the page, we know it will be the lowest cost one
110 /// and can stop looking.
111 final _workList = new HeapPriorityQueue<SolveState>();
112
113 /// The lowest cost solution found so far.
114 SolveState _bestSolution;
115
116 /// Creates a new splitter for [_writer] that tries to fit [chunks] into the
117 /// page width.
118 LineSplitter(this._writer, List<Chunk> chunks, int blockIndentation,
119 int firstLineIndent,
120 {bool flushLeft: false})
121 : _chunks = chunks,
122 // Collect the set of soft rules that we need to select values for.
123 _rules = chunks
124 .map((chunk) => chunk.rule)
125 .where((rule) => rule != null && rule is! HardSplitRule)
126 .toSet()
127 .toList(growable: false),
128 _blockIndentation = blockIndentation,
129 _firstLineIndent = flushLeft ? 0 : firstLineIndent + blockIndentation {
130 // Store the rule's index in the rule so we can get from a chunk to a rule
131 // index quickly.
132 for (var i = 0; i < _rules.length; i++) {
133 _rules[i].index = i;
134 }
135 }
136
137 /// Determine the best way to split the chunks into lines that fit in the
138 /// page, if possible.
139 ///
140 /// Returns a [SplitSet] that defines where each split occurs and the
141 /// indentation of each line.
142 ///
143 /// [firstLineIndent] is the number of characters of whitespace to prefix the
144 /// first line of output with.
145 SplitSet apply() {
146 // Start with a completely unbound, unsplit solution.
147 _workList.add(new SolveState(this, new RuleSet(_rules.length)));
148
149 var attempts = 0;
150 while (!_workList.isEmpty) {
151 var state = _workList.removeFirst();
152
153 if (state.isBetterThan(_bestSolution)) {
154 _bestSolution = state;
155
156 // Since we sort solutions by cost the first solution we find that
157 // fits is the winner.
158 if (_bestSolution.overflowChars == 0) break;
159 }
160
161 if (debug.traceSplitter) {
162 var best = state == _bestSolution ? " (best)" : "";
163 debug.log("$state$best");
164 debug.dumpLines(_chunks, _firstLineIndent, state.splits);
165 debug.log();
166 }
167
168 if (attempts++ > _maxAttempts) break;
169
170 // Try bumping the rule values for rules whose chunks are on long lines.
171 state.expand();
172 }
173
174 if (debug.traceSplitter) {
175 debug.log("$_bestSolution (winner)");
176 debug.dumpLines(_chunks, _firstLineIndent, _bestSolution.splits);
177 debug.log();
178 }
179
180 return _bestSolution.splits;
181 }
182 }
183
184 /// A possibly incomplete solution in the line splitting search space. 12 /// A possibly incomplete solution in the line splitting search space.
185 /// 13 ///
186 /// A single [SolveState] binds some subset of the rules to values while 14 /// A single [SolveState] binds some subset of the rules to values while
187 /// leaving the rest unbound. If every rule is bound, the solve state describes 15 /// leaving the rest unbound. If every rule is bound, the solve state describes
188 /// a complete solution to the line splitting problem. Even if rules are 16 /// a complete solution to the line splitting problem. Even if rules are
189 /// unbound, a state can also usually be used as a solution by treating all 17 /// unbound, a state can also usually be used as a solution by treating all
190 /// unbound rules as unsplit. (The usually is because a state that constrains 18 /// unbound rules as unsplit. (The usually is because a state that constrains
191 /// an unbound rule to split can't be used with that rule unsplit.) 19 /// an unbound rule to split can't be used with that rule unsplit.)
192 /// 20 ///
193 /// From a given solve state, we can explore the search tree to more refined 21 /// From a given solve state, we can explore the search tree to more refined
194 /// solve states by producing new ones that add more bound rules to the current 22 /// solve states by producing new ones that add more bound rules to the current
195 /// state. 23 /// state.
196 class SolveState implements Comparable<SolveState> { 24 class SolveState {
197 final LineSplitter _splitter; 25 final LineSplitter _splitter;
198 final RuleSet _ruleValues; 26 final RuleSet _ruleValues;
199 27
200 /// The unbound rules in this state that can be bound to produce new more 28 /// The unbound rules in this state that can be bound to produce new more
201 /// refined states. 29 /// refined states.
202 /// 30 ///
203 /// Keeping this set small is the key to make the entire line splitter 31 /// Keeping this set small is the key to make the entire line splitter
204 /// perform well. If we consider too make rules at each state, our 32 /// perform well. If we consider too make rules at each state, our
205 /// exploration of the solution space is too branchy and we waste time on 33 /// exploration of the solution space is too branchy and we waste time on
206 /// dead end solutions. 34 /// dead end solutions.
(...skipping 29 matching lines...) Expand all
236 64
237 /// Whether we can treat this state as a complete solution by leaving its 65 /// Whether we can treat this state as a complete solution by leaving its
238 /// unbound rules unsplit. 66 /// unbound rules unsplit.
239 /// 67 ///
240 /// This is generally true but will be false if the state contains any 68 /// This is generally true but will be false if the state contains any
241 /// unbound rules that are constrained to not be zero by other bound rules. 69 /// unbound rules that are constrained to not be zero by other bound rules.
242 /// This avoids picking a solution that leaves those rules at zero when they 70 /// This avoids picking a solution that leaves those rules at zero when they
243 /// aren't allowed to be. 71 /// aren't allowed to be.
244 bool _isComplete = true; 72 bool _isComplete = true;
245 73
74 /// The constraints the bound rules in this state have on the remaining
75 /// unbound rules.
76 Map<Rule, int> _constraints;
77
78 /// The bound rules that appear inside lines also containing unbound rules.
79 ///
80 /// By appearing in the same line, it means these bound rules may affect the
81 /// results of binding those unbound rules. This is used to tell if two
82 /// states may diverge by binding unbound rules or not.
83 Set<Rule> _boundRulesInUnboundLines;
84
246 SolveState(this._splitter, this._ruleValues) { 85 SolveState(this._splitter, this._ruleValues) {
247 _calculateSplits(); 86 _calculateSplits();
248 _calculateCost(); 87 _calculateCost();
249 } 88 }
250 89
251 /// Orders this state relative to [other]. 90 /// Gets the value to use for [rule], either the bound value or `0` if it
252 /// 91 /// isn't bound.
253 /// This is the best-first ordering that the [LineSplitter] uses in its 92 int getValue(Rule rule) {
254 /// worklist. It prefers cheaper states even if they overflow because this 93 if (rule is HardSplitRule) return 0;
255 /// ensures it finds the best solution first as soon as it finds one that
256 /// fits in the page so it can early out.
257 int compareTo(SolveState other) {
258 // TODO(rnystrom): It may be worth sorting by the estimated lowest number
259 // of overflow characters first. That doesn't help in cases where there is
260 // a solution that fits, but may help in corner cases where there is no
261 // fitting solution.
262 94
263 if (splits.cost != other.splits.cost) { 95 return _ruleValues.getValue(rule);
264 return splits.cost.compareTo(other.splits.cost);
265 }
266
267 if (overflowChars != other.overflowChars) {
268 return overflowChars.compareTo(other.overflowChars);
269 }
270
271 // Distinguish states based on the rule values just so that states with the
272 // same cost range but different rule values don't get considered identical
273 // by HeapPriorityQueue.
274 for (var rule in _splitter._rules) {
275 var value = _ruleValues.getValue(rule);
276 var otherValue = other._ruleValues.getValue(rule);
277
278 if (value != otherValue) return value.compareTo(otherValue);
279 }
280
281 // If we get here, this state is identical to [other].
282 return 0;
283 } 96 }
284 97
285 /// Returns `true` if this state is a better solution to use as the final 98 /// Returns `true` if this state is a better solution to use as the final
286 /// result than [other]. 99 /// result than [other].
287 bool isBetterThan(SolveState other) { 100 bool isBetterThan(SolveState other) {
288 // If this state contains an unbound rule that we know can't be left 101 // If this state contains an unbound rule that we know can't be left
289 // unsplit, we can't pick this as a solution. 102 // unsplit, we can't pick this as a solution.
290 if (!_isComplete) return false; 103 if (!_isComplete) return false;
291 104
292 // Anything is better than nothing. 105 // Anything is better than nothing.
293 if (other == null) return true; 106 if (other == null) return true;
294 107
295 // Prefer the solution that fits the most in the page. 108 // Prefer the solution that fits the most in the page.
296 if (overflowChars != other.overflowChars) { 109 if (overflowChars != other.overflowChars) {
297 return overflowChars < other.overflowChars; 110 return overflowChars < other.overflowChars;
298 } 111 }
299 112
300 // Otherwise, prefer the best cost. 113 // Otherwise, prefer the best cost.
301 return splits.cost < other.splits.cost; 114 return splits.cost < other.splits.cost;
302 } 115 }
303 116
117 /// Determines if this state "overlaps" [other].
118 ///
119 /// Two states overlap if they currently have the same score and we can tell
120 /// for certain that they won't diverge as their unbound rules are bound. If
121 /// that's the case, then whichever state is better now (based on their
122 /// currently bound rule values) is the one that will always win, regardless
123 /// of how they get expanded.
124 ///
125 /// In other words, their entire expanded solution trees also overlap. In
126 /// that case, there's no point in expanding both, so we can just pick the
127 /// winner now and discard the other.
128 ///
129 /// For this to be true, we need to prove that binding an unbound rule won't
130 /// affect one state differently from the other. We have to show that they
131 /// are parallel.
132 ///
133 /// Two things could cause this *not* to be the case.
134 ///
135 /// 1. If one state's bound rules place different constraints on the unbound
136 /// rules than the other.
137 ///
138 /// 2. If one state's different bound rules are in the same line as an
139 /// unbound rule. That affects the indentation and length of the line,
140 /// which affects the context where the unbound rule is being chosen.
141 ///
142 /// If neither of these is the case, the states overlap. Returns `<0` if this
143 /// state is better, or `>0` if [other] wins. If the states do not overlap,
144 /// returns `0`.
145 int compareOverlap(SolveState other) {
146 if (!_isOverlapping(other)) return 0;
147
148 // They do overlap, so see which one wins.
149 for (var rule in _splitter.rules) {
150 var value = _ruleValues.getValue(rule);
151 var otherValue = other._ruleValues.getValue(rule);
152
153 if (value != otherValue) return value.compareTo(otherValue);
154 }
155
156 // The way SolveStates are expanded should guarantee that we never generate
157 // the exact same state twice. Getting here implies that that failed.
158 throw "unreachable";
159 }
160
304 /// Enqueues more solve states to consider based on this one. 161 /// Enqueues more solve states to consider based on this one.
305 /// 162 ///
306 /// For each unbound rule in this state that occurred in the first long line, 163 /// For each unbound rule in this state that occurred in the first long line,
307 /// enqueue solve states that bind that rule to each value it can have and 164 /// enqueue solve states that bind that rule to each value it can have and
308 /// bind all previous rules to zero. (In other words, try all subsolutions 165 /// bind all previous rules to zero. (In other words, try all subsolutions
309 /// where that rule becomes the first new rule to split at.) 166 /// where that rule becomes the first new rule to split at.)
310 void expand() { 167 void expand() {
311 var unsplitRules = _ruleValues.clone(); 168 var unsplitRules = _ruleValues.clone();
312 169
313 // Walk down the rules looking for unbound ones to try. 170 // Walk down the rules looking for unbound ones to try.
314 var triedRules = 0; 171 var triedRules = 0;
315 for (var rule in _splitter._rules) { 172 for (var rule in _splitter.rules) {
316 if (_liveRules.contains(rule)) { 173 if (_liveRules.contains(rule)) {
317 // We found one worth trying, so try all of its values. 174 // We found one worth trying, so try all of its values.
318 for (var value = 1; value < rule.numValues; value++) { 175 for (var value = 1; value < rule.numValues; value++) {
319 var boundRules = unsplitRules.clone(); 176 var boundRules = unsplitRules.clone();
320 177
321 var mustSplitRules; 178 var mustSplitRules;
322 var valid = boundRules.tryBind(_splitter._rules, rule, value, (rule) { 179 var valid = boundRules.tryBind(_splitter.rules, rule, value, (rule) {
323 if (mustSplitRules == null) mustSplitRules = []; 180 if (mustSplitRules == null) mustSplitRules = [];
324 mustSplitRules.add(rule); 181 mustSplitRules.add(rule);
325 }); 182 });
326 183
327 // Make sure we don't violate the constraints of the bound rules. 184 // Make sure we don't violate the constraints of the bound rules.
328 if (!valid) continue; 185 if (!valid) continue;
329 186
330 var state = new SolveState(_splitter, boundRules); 187 var state = new SolveState(_splitter, boundRules);
331 188
332 // If some unbound rules are constrained to split, remember that. 189 // If some unbound rules are constrained to split, remember that.
333 if (mustSplitRules != null) { 190 if (mustSplitRules != null) {
334 state._isComplete = false; 191 state._isComplete = false;
335 state._liveRules.addAll(mustSplitRules); 192 state._liveRules.addAll(mustSplitRules);
336 } 193 }
337 194
338 _splitter._workList.add(state); 195 _splitter.enqueue(state);
339 } 196 }
340 197
341 // Stop once we've tried all of the ones we can. 198 // Stop once we've tried all of the ones we can.
342 if (++triedRules == _liveRules.length) break; 199 if (++triedRules == _liveRules.length) break;
343 } 200 }
344 201
345 // Fill in previous unbound rules with zero. 202 // Fill in previous unbound rules with zero.
346 if (!_ruleValues.contains(rule)) { 203 if (!_ruleValues.contains(rule)) {
347 // Pass a dummy callback because zero will never fail. (If it would 204 // Pass a dummy callback because zero will never fail. (If it would
348 // have, that rule would already be bound to some other value.) 205 // have, that rule would already be bound to some other value.)
349 if (!unsplitRules.tryBind(_splitter._rules, rule, 0, (_) {})) { 206 if (!unsplitRules.tryBind(_splitter.rules, rule, 0, (_) {})) {
350 break; 207 break;
351 } 208 }
352 } 209 }
353 } 210 }
354 } 211 }
355 212
213 /// Returns `true` if [other] overlaps this state.
214 bool _isOverlapping(SolveState other) {
215 _ensureOverlapFields();
216 other._ensureOverlapFields();
217
218 // Lines that contain both bound and unbound rules must have the same
219 // bound values.
220 if (_boundRulesInUnboundLines.length !=
221 other._boundRulesInUnboundLines.length) {
222 return false;
223 }
224
225 for (var rule in _boundRulesInUnboundLines) {
226 if (!other._boundRulesInUnboundLines.contains(rule)) return false;
227 if (_ruleValues.getValue(rule) != other._ruleValues.getValue(rule)) {
228 return false;
229 }
230 }
231
232 if (_constraints.length != other._constraints.length) return false;
233
234 for (var rule in _constraints.keys) {
235 if (_constraints[rule] != other._constraints[rule]) return false;
236 }
237
238 return true;
239 }
240
356 /// Calculates the [SplitSet] for this solve state, assuming any unbound 241 /// Calculates the [SplitSet] for this solve state, assuming any unbound
357 /// rules are set to zero. 242 /// rules are set to zero.
358 void _calculateSplits() { 243 void _calculateSplits() {
359 // Figure out which expression nesting levels got split and need to be 244 // Figure out which expression nesting levels got split and need to be
360 // assigned columns. 245 // assigned columns.
361 var usedNestingLevels = new Set(); 246 var usedNestingLevels = new Set();
362 for (var i = 0; i < _splitter._chunks.length - 1; i++) { 247 for (var i = 0; i < _splitter.chunks.length - 1; i++) {
363 var chunk = _splitter._chunks[i]; 248 var chunk = _splitter.chunks[i];
364 if (chunk.rule.isSplit(_getValue(chunk.rule), chunk)) { 249 if (chunk.rule.isSplit(getValue(chunk.rule), chunk)) {
365 usedNestingLevels.add(chunk.nesting); 250 usedNestingLevels.add(chunk.nesting);
366 chunk.nesting.clearTotalUsedIndent(); 251 chunk.nesting.clearTotalUsedIndent();
367 } 252 }
368 } 253 }
369 254
370 for (var nesting in usedNestingLevels) { 255 for (var nesting in usedNestingLevels) {
371 nesting.refreshTotalUsedIndent(usedNestingLevels); 256 nesting.refreshTotalUsedIndent(usedNestingLevels);
372 } 257 }
373 258
374 _splits = new SplitSet(_splitter._chunks.length); 259 _splits = new SplitSet(_splitter.chunks.length);
375 for (var i = 0; i < _splitter._chunks.length - 1; i++) { 260 for (var i = 0; i < _splitter.chunks.length - 1; i++) {
376 var chunk = _splitter._chunks[i]; 261 var chunk = _splitter.chunks[i];
377 if (chunk.rule.isSplit(_getValue(chunk.rule), chunk)) { 262 if (chunk.rule.isSplit(getValue(chunk.rule), chunk)) {
378 var indent = 0; 263 var indent = 0;
379 if (!chunk.flushLeftAfter) { 264 if (!chunk.flushLeftAfter) {
380 // Add in the chunk's indent. 265 // Add in the chunk's indent.
381 indent = _splitter._blockIndentation + chunk.indent; 266 indent = _splitter.blockIndentation + chunk.indent;
382 267
383 // And any expression nesting. 268 // And any expression nesting.
384 indent += chunk.nesting.totalUsedIndent; 269 indent += chunk.nesting.totalUsedIndent;
385 } 270 }
386 271
387 _splits.add(i, indent); 272 _splits.add(i, indent);
388 } 273 }
389 } 274 }
390 } 275 }
391 276
392 /// Gets the value to use for [rule], either the bound value or `0` if it
393 /// isn't bound.
394 int _getValue(Rule rule) {
395 if (rule is HardSplitRule) return 0;
396
397 return _ruleValues.getValue(rule);
398 }
399
400 /// Evaluates the cost (i.e. the relative "badness") of splitting the line 277 /// Evaluates the cost (i.e. the relative "badness") of splitting the line
401 /// into [lines] physical lines based on the current set of rules. 278 /// into [lines] physical lines based on the current set of rules.
402 void _calculateCost() { 279 void _calculateCost() {
403 assert(_splits != null); 280 assert(_splits != null);
404 281
405 // Calculate the length of each line and apply the cost of any spans that 282 // Calculate the length of each line and apply the cost of any spans that
406 // get split. 283 // get split.
407 var cost = 0; 284 var cost = 0;
408 _overflowChars = 0; 285 _overflowChars = 0;
409 286
410 var length = _splitter._firstLineIndent; 287 var length = _splitter.firstLineIndent;
411 288
412 // The unbound rules in use by the current line. This will be null after 289 // The unbound rules in use by the current line. This will be null after
413 // the first long line has completed. 290 // the first long line has completed.
414 var currentLineRules = []; 291 var currentLineRules = [];
415 292
416 endLine(int end) { 293 endLine(int end) {
417 // Track lines that went over the length. It is only rules contained in 294 // Track lines that went over the length. It is only rules contained in
418 // long lines that we may want to split. 295 // long lines that we may want to split.
419 if (length > _splitter._writer.pageWidth) { 296 if (length > _splitter.writer.pageWidth) {
420 _overflowChars += length - _splitter._writer.pageWidth; 297 _overflowChars += length - _splitter.writer.pageWidth;
421 298
422 // Only try rules that are in the first long line, since we know at 299 // Only try rules that are in the first long line, since we know at
423 // least one of them *will* be split. 300 // least one of them *will* be split.
424 if (currentLineRules != null && currentLineRules.isNotEmpty) { 301 if (currentLineRules != null && currentLineRules.isNotEmpty) {
425 _liveRules.addAll(currentLineRules); 302 _liveRules.addAll(currentLineRules);
426 currentLineRules = null; 303 currentLineRules = null;
427 } 304 }
428 } else { 305 } else {
429 // The line fit, so don't keep track of its rules. 306 // The line fit, so don't keep track of its rules.
430 if (currentLineRules != null) { 307 if (currentLineRules != null) {
431 currentLineRules.clear(); 308 currentLineRules.clear();
432 } 309 }
433 } 310 }
434 } 311 }
435 312
436 // The set of spans that contain chunks that ended up splitting. We store 313 // The set of spans that contain chunks that ended up splitting. We store
437 // these in a set so a span's cost doesn't get double-counted if more than 314 // these in a set so a span's cost doesn't get double-counted if more than
438 // one split occurs in it. 315 // one split occurs in it.
439 var splitSpans = new Set(); 316 var splitSpans = new Set();
440 317
441 for (var i = 0; i < _splitter._chunks.length; i++) { 318 for (var i = 0; i < _splitter.chunks.length; i++) {
442 var chunk = _splitter._chunks[i]; 319 var chunk = _splitter.chunks[i];
443 320
444 length += chunk.text.length; 321 length += chunk.text.length;
445 322
446 // Ignore the split after the last chunk. 323 // Ignore the split after the last chunk.
447 if (i == _splitter._chunks.length - 1) break; 324 if (i == _splitter.chunks.length - 1) break;
448 325
449 if (_splits.shouldSplitAt(i)) { 326 if (_splits.shouldSplitAt(i)) {
450 endLine(i); 327 endLine(i);
451 328
452 splitSpans.addAll(chunk.spans); 329 splitSpans.addAll(chunk.spans);
453 330
454 // Include the cost of the nested block. 331 // Include the cost of the nested block.
455 if (chunk.blockChunks.isNotEmpty) { 332 if (chunk.blockChunks.isNotEmpty) {
456 cost += 333 cost +=
457 _splitter._writer.formatBlock(chunk, _splits.getColumn(i)).cost; 334 _splitter.writer.formatBlock(chunk, _splits.getColumn(i)).cost;
458 } 335 }
459 336
460 // Start the new line. 337 // Start the new line.
461 length = _splits.getColumn(i); 338 length = _splits.getColumn(i);
462 } else { 339 } else {
463 if (chunk.spaceWhenUnsplit) length++; 340 if (chunk.spaceWhenUnsplit) length++;
464 341
465 // Include the nested block inline, if any. 342 // Include the nested block inline, if any.
466 length += chunk.unsplitBlockLength; 343 length += chunk.unsplitBlockLength;
467 344
468 // If we might be in the first overly long line, keep track of any 345 // If we might be in the first overly long line, keep track of any
469 // unbound rules we encounter. These are ones that we'll want to try to 346 // unbound rules we encounter. These are ones that we'll want to try to
470 // bind to shorten the long line. 347 // bind to shorten the long line.
471 if (currentLineRules != null && 348 if (currentLineRules != null &&
472 chunk.rule != null && 349 chunk.rule != null &&
473 !chunk.isHardSplit && 350 !chunk.isHardSplit &&
474 !_ruleValues.contains(chunk.rule)) { 351 !_ruleValues.contains(chunk.rule)) {
475 currentLineRules.add(chunk.rule); 352 currentLineRules.add(chunk.rule);
476 } 353 }
477 } 354 }
478 } 355 }
479 356
480 // Add the costs for the rules that split. 357 // Add the costs for the rules that split.
481 _ruleValues.forEach(_splitter._rules, (rule, value) { 358 _ruleValues.forEach(_splitter.rules, (rule, value) {
482 // A rule may be bound to zero if another rule constrains it to not split. 359 // A rule may be bound to zero if another rule constrains it to not split.
483 if (value != 0) cost += rule.cost; 360 if (value != 0) cost += rule.cost;
484 }); 361 });
485 362
486 // Add the costs for the spans containing splits. 363 // Add the costs for the spans containing splits.
487 for (var span in splitSpans) cost += span.cost; 364 for (var span in splitSpans) cost += span.cost;
488 365
489 // Finish the last line. 366 // Finish the last line.
490 endLine(_splitter._chunks.length); 367 endLine(_splitter.chunks.length);
491 368
492 _splits.setCost(cost); 369 _splits.setCost(cost);
493 } 370 }
494 371
372 /// Lazily initializes the fields needed to compare two states for overlap.
373 ///
374 /// We do this lazily because the calculation is a bit slow, and is only
375 /// needed when we have two states with the same score.
376 void _ensureOverlapFields() {
377 if (_constraints != null) return;
378
379 _calculateConstraints();
380 _calculateBoundRulesInUnboundLines();
381 }
382
383 /// Initializes [_constraints] with any constraints the bound rules place on
384 /// the unbound rules.
385 void _calculateConstraints() {
386 _constraints = {};
387
388 var unboundRules = [];
389 var boundRules = [];
390
391 for (var rule in _splitter.rules) {
392 if (_ruleValues.contains(rule)) {
393 boundRules.add(rule);
394 } else {
395 unboundRules.add(rule);
396 }
397 }
398
399 for (var bound in boundRules) {
400 var value = _ruleValues.getValue(bound);
401
402 for (var unbound in unboundRules) {
403 var constraint = bound.constrain(value, unbound);
404 if (constraint != null) {
405 _constraints[unbound] = constraint;
406 }
407 }
408 }
409 }
410
411 void _calculateBoundRulesInUnboundLines() {
412 _boundRulesInUnboundLines = new Set();
413
414 var boundInLine = new Set();
415 var hasUnbound = false;
416
417 for (var i = 0; i < _splitter.chunks.length - 1; i++) {
418
419 if (splits.shouldSplitAt(i)) {
420 if (hasUnbound) _boundRulesInUnboundLines.addAll(boundInLine);
421
422 boundInLine.clear();
423 hasUnbound = false;
424 }
425
426 var rule = _splitter.chunks[i].rule;
427 if (rule != null && rule is! HardSplitRule) {
428 if (_ruleValues.contains(rule)) {
429 boundInLine.add(rule);
430 } else {
431 hasUnbound = true;
432 }
433 }
434 }
435
436 if (hasUnbound) _boundRulesInUnboundLines.addAll(boundInLine);
437 }
438
495 String toString() { 439 String toString() {
496 var buffer = new StringBuffer(); 440 var buffer = new StringBuffer();
497 441
498 buffer.writeAll( 442 buffer.writeAll(
499 _splitter._rules.map((rule) { 443 _splitter.rules.map((rule) {
500 var valueLength = "${rule.fullySplitValue}".length; 444 var valueLength = "${rule.fullySplitValue}".length;
501 445
502 var value = "?"; 446 var value = "?";
503 if (_ruleValues.contains(rule)) { 447 if (_ruleValues.contains(rule)) {
504 value = "${_ruleValues.getValue(rule)}"; 448 value = "${_ruleValues.getValue(rule)}";
505 } 449 }
506 450
507 value = value.padLeft(valueLength); 451 value = value.padLeft(valueLength);
508 if (_liveRules.contains(rule)) { 452 if (_liveRules.contains(rule)) {
509 value = debug.bold(value); 453 value = debug.bold(value);
510 } else { 454 } else {
511 value = debug.gray(value); 455 value = debug.gray(value);
512 } 456 }
513 457
514 return value; 458 return value;
515 }), 459 }),
516 " "); 460 " ");
517 461
518 buffer.write(" \$${splits.cost}"); 462 buffer.write(" \$${splits.cost}");
519 463
520 if (overflowChars > 0) buffer.write(" (${overflowChars} over)"); 464 if (overflowChars > 0) buffer.write(" (${overflowChars} over)");
521 if (!_isComplete) buffer.write(" (incomplete)"); 465 if (!_isComplete) buffer.write(" (incomplete)");
522 if (splits == null) buffer.write(" invalid"); 466 if (splits == null) buffer.write(" invalid");
523 467
524 return buffer.toString(); 468 return buffer.toString();
525 } 469 }
526 } 470 }
OLDNEW
« no previous file with comments | « lib/src/line_splitting/rule_set.dart ('k') | lib/src/line_splitting/solve_state_queue.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698