Skip to content

Commit

Permalink
Format binary expressions with the new back-end. (#1267)
Browse files Browse the repository at this point in the history
Format binary expressions with the new back-end.

This includes all arithmetic, logic, comparison, relational, type test,
and bitwise operators.

It doesn't include assignment operators, because those are right
associative and need some special handling for how the RHS is indented.
  • Loading branch information
munificent authored Sep 15, 2023
1 parent 251e094 commit cf8aef9
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 7 deletions.
33 changes: 27 additions & 6 deletions lib/src/front_end/ast_node_visitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitAsExpression(AsExpression node) {
throw UnimplementedError();
createInfix(node.expression, node.asOperator, node.type);
}

@override
Expand Down Expand Up @@ -96,7 +96,14 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitBinaryExpression(BinaryExpression node) {
throw UnimplementedError();
createInfixChain<BinaryExpression>(
node,
precedence: node.operator.type.precedence,
(expression) => (
expression.leftOperand,
expression.operator,
expression.rightOperand
));
}

@override
Expand Down Expand Up @@ -290,7 +297,8 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitExpressionStatement(ExpressionStatement node) {
throw UnimplementedError();
visit(node.expression);
token(node.semicolon);
}

@override
Expand Down Expand Up @@ -443,7 +451,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitIntegerLiteral(IntegerLiteral node) {
throw UnimplementedError();
token(node.literal);
}

@override
Expand All @@ -458,7 +466,11 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitIsExpression(IsExpression node) {
throw UnimplementedError();
createInfix(
node.expression,
node.isOperator,
operator2: node.notOperator,
node.type);
}

@override
Expand Down Expand Up @@ -541,7 +553,16 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitNamedType(NamedType node) {
throw UnimplementedError();
// TODO(tall): Handle import prefix.
if (node.importPrefix != null) throw UnimplementedError();

token(node.name2);

// TODO(tall): Handle type arguments.
if (node.typeArguments != null) throw UnimplementedError();

// TODO(tall): Handle nullable types.
if (node.question != null) throw UnimplementedError();
}

@override
Expand Down
57 changes: 56 additions & 1 deletion lib/src/front_end/piece_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import '../piece/postfix.dart';
import '../piece/sequence.dart';
import 'piece_writer.dart';

/// Record type for a destructured binary operator-like syntactic construct.
typedef BinaryOperation = (AstNode left, Token operator, AstNode right);

/// Utility methods for creating pieces that share formatting logic across
/// multiple parts of the language.
///
Expand Down Expand Up @@ -134,19 +137,24 @@ mixin PieceFactory {
/// If [hanging] is `true` then the operator goes at the end of the first
/// line, like `+`. Otherwise, it goes at the beginning of the second, like
/// `as`.
///
/// The [operator2] parameter may be passed if the "operator" is actually two
/// separate tokens, as in `foo is! Bar`.
void createInfix(AstNode left, Token operator, AstNode right,
{bool hanging = false}) {
{bool hanging = false, Token? operator2}) {
var operands = <Piece>[];
visit(left);
operands.add(writer.pop());

if (hanging) {
writer.space();
token(operator);
token(operator2);
writer.split();
} else {
writer.split();
token(operator);
token(operator2);
writer.space();
}

Expand All @@ -155,6 +163,53 @@ mixin PieceFactory {
writer.push(InfixPiece(operands));
}

/// Creates a chained infix operation: a binary operator expression, or
/// binary pattern.
///
/// In a tree of binary AST nodes, all operators at the same precedence are
/// treated as a single chain of operators that either all split or none do.
/// Operands within those (which may themselves be chains of higher
/// precedence binary operators) are then formatted independently.
///
/// [T] is the type of node being visited and [destructure] is a callback
/// that takes one of those and yields the operands and operator. We need
/// this since there's no interface shared by the various binary operator
/// AST nodes.
///
/// If [precedence] is given, then this only flattens binary nodes with that
/// same precedence.
void createInfixChain<T extends AstNode>(
T node, BinaryOperation Function(T node) destructure,
{int? precedence}) {
var operands = <Piece>[];

void traverse(AstNode e) {
if (e is! T) {
visit(e);
operands.add(writer.pop());
} else {
var (left, operator, right) = destructure(e);
if (precedence != null && operator.type.precedence != precedence) {
// Binary node, but a different precedence, so don't flatten.
visit(e);
operands.add(writer.pop());
} else {
traverse(left);

writer.space();
token(operator);

writer.split();
traverse(right);
}
}
}

traverse(node);

writer.push(InfixPiece(operands));
}

/// Emit [token], along with any comments and formatted whitespace that comes
/// before it.
///
Expand Down
118 changes: 118 additions & 0 deletions test/expression/binary.stmt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
40 columns |
>>> Multiplicative operators.
1*2/3~/4%5;
<<<
1 * 2 / 3 ~/ 4 % 5;
>>> Additive operators.
1+2-3;
<<<
1 + 2 - 3;
>>> Shift operators.
1<<2>>3>>>4;
<<<
1 << 2 >> 3 >>> 4;
>>> Bitwise operators.
1&2^3|4;
<<<
1 & 2 ^ 3 | 4;
>>> Relation operators (which are not associative in Dart).
1<2;
<<<
1 < 2;
>>>
1>2;
<<<
1 > 2;
>>>
1<=2;
<<<
1 <= 2;
>>>
1>=2;
<<<
1 >= 2;
>>> Equality operators (which are not associative in Dart).
1==2;
<<<
1 == 2;
>>> Equality operators.
1!=2;
<<<
1 != 2;
>>> Logical operators.
1&&2||3;
<<<
1 && 2 || 3;
>>> If-null operator.
foo??bar;
<<<
foo ?? bar;
>>> Unsplit operators with mixed precedence.
1+2/3-4*5%6<<7;
<<<
1 + 2 / 3 - 4 * 5 % 6 << 7;
>>> If any operator splits, they all do.
operand1 + operand2 + operand3 + operand4;
<<<
operand1 +
operand2 +
operand3 +
operand4;
>>> Mixed multiplicative operators split together.
longName * longName / longName % longName ~/ longName;
<<<
longName *
longName /
longName %
longName ~/
longName;
>>> Mixed additive operators split together.
longName + longName - longName + longName - longName;
<<<
longName +
longName -
longName +
longName -
longName;
>>> Mixed shift operators split together.
longName >> longName << longName >> longName >>> longName;
<<<
longName >>
longName <<
longName >>
longName >>>
longName;
>>> Mixed ascending precedence.
b___________________ || a______________ && a______________ == a______________ >
a______________ + a______________;
<<<
b___________________ ||
a______________ &&
a______________ ==
a______________ >
a______________ +
a______________;
>>> Mixed descending precedence.
b___________________ + a_______________ > a______________ == a______________ &&
a______________ || a______________;
<<<
b___________________ +
a_______________ >
a______________ ==
a______________ &&
a______________ ||
a______________;
>>> Mixture of same and different precedence.
veryLongIdentifier + veryLongIdentifier / veryLongIdentifier *
veryLongIdentifier - veryLongIdentifier * veryLongIdentifier +
veryLongIdentifier / veryLongIdentifier - veryLongIdentifier;
<<<
veryLongIdentifier +
veryLongIdentifier /
veryLongIdentifier *
veryLongIdentifier -
veryLongIdentifier *
veryLongIdentifier +
veryLongIdentifier /
veryLongIdentifier -
veryLongIdentifier;
28 changes: 28 additions & 0 deletions test/expression/type_test.stmt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
40 columns |
>>>
foo as Bar;
<<<
foo as Bar;
>>>
foo is Bar;
<<<
foo is Bar;
>>>
foo is ! Bar;
<<<
foo is! Bar;
>>> Split `as` before operator.
extremelyLongIdentifier as VeryLongTypeName;
<<<
extremelyLongIdentifier
as VeryLongTypeName;
>>> Split `is` before operator.
extremelyLongIdentifier is VeryLongTypeName;
<<<
extremelyLongIdentifier
is VeryLongTypeName;
>>> Split `is!` before operator.
extremelyLongIdentifier is ! VeryLongTypeName;
<<<
extremelyLongIdentifier
is! VeryLongTypeName;
5 changes: 5 additions & 0 deletions test/statement/expression.stmt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
40 columns |
>>> No space before ";".
expression ;
<<<
expression;
2 changes: 2 additions & 0 deletions test/tall_format_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import 'package:test/test.dart';
import 'utils.dart';

void main() async {
await testDirectory('expression', tall: true);
await testDirectory('statement', tall: true);
await testDirectory('top_level', tall: true);

// TODO(tall): The old formatter_test.dart has tests here for things like
Expand Down

0 comments on commit cf8aef9

Please sign in to comment.