Skip to content

Commit 6641bde

Browse files
authored
Flatten chained conditionals. (#1489)
When testing the new formatter on a large corpus, I noticed the solver would get stuck on large conditional chains because (unfortunately), we can't separately format the else clause of a split conditional expression. By merging long conditional chains into a single InfixPiece, we can separately format all but the very last dangling else clause. While I was at it, I also put a hard cap in the number of solutions the solver will try in case it still gets stuck. The old formatter has a similar limit. It's rare for real-world code to hit this limit in the new solver, but it's better than getting totally stuck when it happens.
1 parent ce04d38 commit 6641bde

File tree

10 files changed

+171
-48
lines changed

10 files changed

+171
-48
lines changed

benchmark/case/conditional.expect

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
void securityItem() {
2+
return SelectableText(
3+
itemSecurityScheme.securitySchemeType == SecuritySchemeType.QueryAPIKey
4+
? Constants.oneTwoThreeTxt
5+
: itemSecurityScheme.securitySchemeType ==
6+
SecuritySchemeType.HeaderAPIKey ||
7+
itemSecurityScheme.securitySchemeType ==
8+
SecuritySchemeType.CookieAPIKey
9+
? Constants.oneTwoThreeTxt
10+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.BasicHTTP
11+
? Constants.demoUsernameTxt
12+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.BearerHTTP
13+
? Constants.oneTwoThreeTxt
14+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.DigestHTTP
15+
? Constants.digestDemoTxt
16+
: itemSecurityScheme.securitySchemeType ==
17+
SecuritySchemeType.OAuth2PasswordFlow ||
18+
itemSecurityScheme.securitySchemeType ==
19+
SecuritySchemeType.OAuth2ClientFlow
20+
? Constants.emptyTxt
21+
: itemSecurityScheme.securitySchemeType ==
22+
SecuritySchemeType.OAuth2ImplicitFlow
23+
? Constants.emptyTxt
24+
: itemSecurityScheme.securitySchemeType ==
25+
SecuritySchemeType.OAuth2CodeFlow
26+
? Constants.emptyTxt
27+
: Constants.emptyTxt,
28+
);
29+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
void securityItem() {
2+
return SelectableText(
3+
itemSecurityScheme.securitySchemeType == SecuritySchemeType.QueryAPIKey
4+
? Constants.oneTwoThreeTxt
5+
: itemSecurityScheme.securitySchemeType ==
6+
SecuritySchemeType.HeaderAPIKey ||
7+
itemSecurityScheme.securitySchemeType ==
8+
SecuritySchemeType.CookieAPIKey
9+
? Constants.oneTwoThreeTxt
10+
: itemSecurityScheme.securitySchemeType ==
11+
SecuritySchemeType.BasicHTTP
12+
? Constants.demoUsernameTxt
13+
: itemSecurityScheme.securitySchemeType ==
14+
SecuritySchemeType.BearerHTTP
15+
? Constants.oneTwoThreeTxt
16+
: itemSecurityScheme.securitySchemeType ==
17+
SecuritySchemeType.DigestHTTP
18+
? Constants.digestDemoTxt
19+
: itemSecurityScheme.securitySchemeType ==
20+
SecuritySchemeType.OAuth2PasswordFlow ||
21+
itemSecurityScheme.securitySchemeType ==
22+
SecuritySchemeType.OAuth2ClientFlow
23+
? Constants.emptyTxt
24+
: itemSecurityScheme.securitySchemeType ==
25+
SecuritySchemeType.OAuth2ImplicitFlow
26+
? Constants.emptyTxt
27+
: itemSecurityScheme.securitySchemeType ==
28+
SecuritySchemeType.OAuth2CodeFlow
29+
? Constants.emptyTxt
30+
: Constants.emptyTxt,
31+
);
32+
}

benchmark/case/conditional.unit

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
void securityItem() {
2+
return SelectableText(
3+
itemSecurityScheme.securitySchemeType == SecuritySchemeType.QueryAPIKey
4+
? Constants.oneTwoThreeTxt
5+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.HeaderAPIKey ||
6+
itemSecurityScheme.securitySchemeType == SecuritySchemeType.CookieAPIKey
7+
? Constants.oneTwoThreeTxt
8+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.BasicHTTP
9+
? Constants.demoUsernameTxt
10+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.BearerHTTP
11+
? Constants.oneTwoThreeTxt
12+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.DigestHTTP
13+
? Constants.digestDemoTxt
14+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.OAuth2PasswordFlow ||
15+
itemSecurityScheme.securitySchemeType == SecuritySchemeType.OAuth2ClientFlow
16+
? Constants.emptyTxt
17+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.OAuth2ImplicitFlow
18+
? Constants.emptyTxt
19+
: itemSecurityScheme.securitySchemeType == SecuritySchemeType.OAuth2CodeFlow
20+
? Constants.emptyTxt
21+
: Constants.emptyTxt,
22+
);
23+
}

lib/src/back_end/solver.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import '../profile.dart';
99
import 'solution.dart';
1010
import 'solution_cache.dart';
1111

12+
/// To ensure the solver doesn't go totally pathological on giant code, we cap
13+
/// it at a fixed number of attempts.
14+
///
15+
/// If the optimal solution isn't found after this many tries, it just uses the
16+
/// best it found so far.
17+
const _maxAttempts = 10000;
18+
1219
/// Selects states for each piece in a tree of pieces to find the best set of
1320
/// line splits that minimizes overflow characters and line splitting costs.
1421
///
@@ -85,18 +92,17 @@ class Solver {
8592
// The lowest cost solution found so far that does overflow.
8693
var best = solution;
8794

88-
var tries = 0;
95+
var attempts = 0;
8996

90-
// TODO(perf): Consider bailing out after a certain maximum number of tries,
91-
// so that it outputs suboptimal formatting instead of hanging entirely.
92-
while (_queue.isNotEmpty) {
97+
while (_queue.isNotEmpty && attempts < _maxAttempts) {
9398
Profile.begin('Solver dequeue');
9499
var solution = _queue.removeFirst();
95100
Profile.end('Solver dequeue');
96101

102+
attempts++;
103+
97104
if (debug.traceSolver) {
98-
tries++;
99-
debug.log(debug.bold('Try #$tries $solution'));
105+
debug.log(debug.bold('Try #$attempts $solution'));
100106
debug.log(solution.text);
101107
debug.log('');
102108
}

lib/src/front_end/ast_node_visitor.dart

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -324,21 +324,44 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {
324324
// conditional expression to split.
325325
var leadingComments = pieces.takeCommentsBefore(node.firstNonCommentToken);
326326

327-
var condition = nodePiece(node.condition);
327+
// Flatten a series of else-if-like chained conditionals into a single long
328+
// infix piece. This produces a flattened style like:
329+
//
330+
// condition
331+
// ? thenBranch
332+
// : condition2
333+
// ? thenBranch2
334+
// : elseBranch;
335+
//
336+
// This (arguably) looks nicer. More importantly, it means that all but the
337+
// last operand can be formatted separately, which is important to avoid
338+
// pathological performance in the solved with long nested conditional
339+
// chains.
340+
var operands = [nodePiece(node.condition)];
341+
342+
void addOperand(Token operator, Expression operand) {
343+
operands.add(pieces.build(() {
344+
pieces.token(operator);
345+
pieces.space();
346+
pieces.visit(operand, context: NodeContext.conditionalBranch);
347+
}));
348+
}
328349

329-
var thenPiece = pieces.build(() {
330-
pieces.token(node.question);
331-
pieces.space();
332-
pieces.visit(node.thenExpression, context: NodeContext.conditionalBranch);
333-
});
350+
var conditional = node;
351+
while (true) {
352+
addOperand(conditional.question, conditional.thenExpression);
334353

335-
var elsePiece = pieces.build(() {
336-
pieces.token(node.colon);
337-
pieces.space();
338-
pieces.visit(node.elseExpression, context: NodeContext.conditionalBranch);
339-
});
354+
var elseBranch = conditional.elseExpression;
355+
if (elseBranch is ConditionalExpression) {
356+
addOperand(conditional.colon, elseBranch.condition);
357+
conditional = elseBranch;
358+
} else {
359+
addOperand(conditional.colon, conditional.elseExpression);
360+
break;
361+
}
362+
}
340363

341-
var piece = InfixPiece(leadingComments, [condition, thenPiece, elsePiece]);
364+
var piece = InfixPiece(leadingComments, operands);
342365

343366
// If conditional expressions are directly nested, force them all to split,
344367
// both parents and children.

test/tall/expression/condition.stmt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,22 @@ var kind =
4343
a
4444
? b
4545
: c
46-
? d
47-
: e;
46+
? d
47+
: e;
4848
>>> Don't force split conditionals when indirectly nested.
4949
var kind = a ? b : (c ? d : e);
5050
<<<
51-
var kind = a ? b : (c ? d : e);
51+
var kind = a ? b : (c ? d : e);
52+
>>> Flatten a chain of else-if conditionals.
53+
var kind = c1 ? e1 : c2 ? e2 : c3 ? e3 : c4 ? e4 : e5;
54+
<<<
55+
var kind =
56+
c1
57+
? e1
58+
: c2
59+
? e2
60+
: c3
61+
? e3
62+
: c4
63+
? e4
64+
: e5;

test/tall/regression/0400/0407.unit

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ receiver
3232
_total == 0
3333
? ""
3434
: _chartType == "PieChart"
35-
? _formatter.formatAsPercent(
36-
item.value / _total,
37-
fractionDigits: 1,
38-
)
39-
: _formatter.formatValue(item.value, item.valueType);
35+
? _formatter.formatAsPercent(
36+
item.value / _total,
37+
fractionDigits: 1,
38+
)
39+
: _formatter.formatValue(item.value, item.valueType);
4040
}
4141
>>> (indent 6)
4242
main() {
@@ -55,9 +55,9 @@ receiver
5555
_total == 0
5656
? ""
5757
: _chartType == "PieChart"
58-
? _formatter.formatAsPercent(
59-
item.value / _total,
60-
fractionDigits: 1,
61-
)
62-
: _formatter.formatValue(item.value, item.valueType);
58+
? _formatter.formatAsPercent(
59+
item.value / _total,
60+
fractionDigits: 1,
61+
)
62+
: _formatter.formatValue(item.value, item.valueType);
6363
}

test/tall/regression/0700/0713.stmt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ String type =
88
status == 'OK'
99
? 'notices'
1010
: status == 'NO'
11-
? 'warnings'
12-
: status == 'BAD'
13-
? 'errors'
14-
: '';
11+
? 'warnings'
12+
: status == 'BAD'
13+
? 'errors'
14+
: '';

test/tall/regression/0700/0722.stmt

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,12 @@ Widget(
2222
project.locked
2323
? Icon(Icons.lock)
2424
: project.fav
25-
? Icon(Icons.star)
26-
: project.taps == null
27-
? Icon(Icons.notifications)
28-
: Text(
29-
suffixNumber(project.taps),
30-
textAlign: TextAlign.center,
31-
style: TextStyle(
32-
fontSize: 18.0,
33-
fontWeight: FontWeight.w600,
34-
),
35-
),
25+
? Icon(Icons.star)
26+
: project.taps == null
27+
? Icon(Icons.notifications)
28+
: Text(
29+
suffixNumber(project.taps),
30+
textAlign: TextAlign.center,
31+
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w600),
32+
),
3633
);

test/tall/regression/0900/0927.unit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ class C {
99
_currentSunAngleDeg < 0
1010
? 1
1111
: _currentSunAngleDeg < 10
12-
? 2
13-
: 3;
12+
? 2
13+
: 3;
1414
}

0 commit comments

Comments
 (0)