Commit 1c79eea125

Andrew Kelley <andrew@ziglang.org>
2021-02-10 01:23:57
zig fmt: while loops
1 parent bcafc51
Changed files (4)
lib/std/zig/ast.zig
@@ -760,6 +760,13 @@ pub const Tree = struct {
                 n = extra.sentinel;
             },
 
+            .Continue => {
+                if (datas[n].lhs != 0) {
+                    return datas[n].lhs + end_offset;
+                } else {
+                    return main_tokens[n] + end_offset;
+                }
+            },
             .Break => {
                 if (datas[n].rhs != 0) {
                     n = datas[n].rhs;
@@ -837,6 +844,21 @@ pub const Tree = struct {
                 n = max_node;
                 end_offset += max_offset;
             },
+            .WhileCont => {
+                const extra = tree.extraData(datas[n].rhs, Node.WhileCont);
+                assert(extra.then_expr != 0);
+                n = extra.then_expr;
+            },
+            .While => {
+                const extra = tree.extraData(datas[n].rhs, Node.While);
+                assert(extra.else_expr != 0);
+                n = extra.else_expr;
+            },
+            .If => {
+                const extra = tree.extraData(datas[n].rhs, Node.If);
+                assert(extra.else_expr != 0);
+                n = extra.else_expr;
+            },
 
             // These are not supported by lastToken() because implementation would
             // require recursion due to the optional comma followed by rbrace.
@@ -851,13 +873,9 @@ pub const Tree = struct {
 
             .TaggedUnionEnumTag => unreachable, // TODO
             .TaggedUnionEnumTagComma => unreachable, // TODO
-            .If => unreachable, // TODO
-            .Continue => unreachable, // TODO
             .SwitchRange => unreachable, // TODO
             .ArrayType => unreachable, // TODO
             .ArrayTypeSentinel => unreachable, // TODO
-            .WhileCont => unreachable, // TODO
-            .While => unreachable, // TODO
             .ForSimple => unreachable, // TODO
             .For => unreachable, // TODO
             .ErrorValue => unreachable, // TODO
@@ -1404,6 +1422,41 @@ pub const Tree = struct {
         });
     }
 
+    pub fn whileSimple(tree: Tree, node: Node.Index) full.While {
+        const data = tree.nodes.items(.data)[node];
+        return tree.fullWhile(.{
+            .while_token = tree.nodes.items(.main_token)[node],
+            .cond_expr = data.lhs,
+            .cont_expr = 0,
+            .then_expr = data.rhs,
+            .else_expr = 0,
+        });
+    }
+
+    pub fn whileCont(tree: Tree, node: Node.Index) full.While {
+        const data = tree.nodes.items(.data)[node];
+        const extra = tree.extraData(data.rhs, Node.WhileCont);
+        return tree.fullWhile(.{
+            .while_token = tree.nodes.items(.main_token)[node],
+            .cond_expr = data.lhs,
+            .cont_expr = extra.cont_expr,
+            .then_expr = extra.then_expr,
+            .else_expr = 0,
+        });
+    }
+
+    pub fn whileFull(tree: Tree, node: Node.Index) full.While {
+        const data = tree.nodes.items(.data)[node];
+        const extra = tree.extraData(data.rhs, Node.While);
+        return tree.fullWhile(.{
+            .while_token = tree.nodes.items(.main_token)[node],
+            .cond_expr = data.lhs,
+            .cont_expr = extra.cont_expr,
+            .then_expr = extra.then_expr,
+            .else_expr = extra.else_expr,
+        });
+    }
+
     fn fullVarDecl(tree: Tree, info: full.VarDecl.Ast) full.VarDecl {
         const token_tags = tree.tokens.items(.tag);
         var result: full.VarDecl = .{
@@ -1623,6 +1676,41 @@ pub const Tree = struct {
 
         return result;
     }
+
+    fn fullWhile(tree: Tree, info: full.While.Ast) full.While {
+        const token_tags = tree.tokens.items(.tag);
+        var result: full.While = .{
+            .ast = info,
+            .inline_token = null,
+            .label_token = null,
+            .payload_token = null,
+            .else_token = undefined,
+            .error_token = null,
+        };
+        var tok_i = info.while_token - 1;
+        if (token_tags[tok_i] == .Keyword_inline) {
+            result.inline_token = tok_i;
+            tok_i -= 1;
+        }
+        if (token_tags[tok_i] == .Colon and
+            token_tags[tok_i - 1] == .Identifier)
+        {
+            result.label_token = tok_i - 1;
+        }
+        const last_cond_token = tree.lastToken(info.cond_expr);
+        if (token_tags[last_cond_token + 2] == .Pipe) {
+            result.payload_token = last_cond_token + 3;
+        }
+        if (info.else_expr != 0) {
+            // then_expr else |x|
+            //           ^    ^
+            result.else_token = tree.lastToken(info.then_expr) + 1;
+            if (token_tags[result.else_token + 1] == .Pipe) {
+                result.error_token = result.else_token + 2;
+            }
+        }
+        return result;
+    }
 };
 
 /// Fully assembled AST node information.
@@ -1645,12 +1733,12 @@ pub const full = struct {
     };
 
     pub const If = struct {
-        // Points to the first token after the `|`. Will either be an identifier or
-        // a `*` (with an identifier immediately after it).
+        /// Points to the first token after the `|`. Will either be an identifier or
+        /// a `*` (with an identifier immediately after it).
         payload_token: ?TokenIndex,
-        // Points to the identifier after the `|`.
+        /// Points to the identifier after the `|`.
         error_token: ?TokenIndex,
-        // Populated only if else_expr != 0.
+        /// Populated only if else_expr != 0.
         else_token: TokenIndex,
         ast: Ast,
 
@@ -1662,6 +1750,24 @@ pub const full = struct {
         };
     };
 
+    pub const While = struct {
+        ast: Ast,
+        inline_token: ?TokenIndex,
+        label_token: ?TokenIndex,
+        payload_token: ?TokenIndex,
+        error_token: ?TokenIndex,
+        /// Populated only if else_expr != 0.
+        else_token: TokenIndex,
+
+        pub const Ast = struct {
+            while_token: TokenIndex,
+            cond_expr: Node.Index,
+            cont_expr: Node.Index,
+            then_expr: Node.Index,
+            else_expr: Node.Index,
+        };
+    };
+
     pub const ContainerField = struct {
         comptime_token: ?TokenIndex,
         ast: Ast,
@@ -2270,9 +2376,9 @@ pub const Node = struct {
         /// `if (lhs) rhs`.
         /// `if (lhs) |a| rhs`.
         IfSimple,
-        /// `if (lhs) a else b`. `if_list[rhs]`.
-        /// `if (lhs) |x| a else b`. `if_list[rhs]`.
-        /// `if (lhs) |x| a else |y| b`. `if_list[rhs]`.
+        /// `if (lhs) a else b`. `If[rhs]`.
+        /// `if (lhs) |x| a else b`. `If[rhs]`.
+        /// `if (lhs) |x| a else |y| b`. `If[rhs]`.
         If,
         /// `suspend lhs`. lhs can be omitted. rhs is unused.
         Suspend,
@@ -2497,13 +2603,13 @@ pub const Node = struct {
     };
 
     pub const While = struct {
-        continue_expr: Index,
+        cont_expr: Index,
         then_expr: Index,
         else_expr: Index,
     };
 
     pub const WhileCont = struct {
-        continue_expr: Index,
+        cont_expr: Index,
         then_expr: Index,
     };
 
lib/std/zig/parse.zig
@@ -1085,7 +1085,7 @@ const Parser = struct {
         const condition = try p.expectExpr();
         _ = try p.expectToken(.RParen);
         const then_payload = try p.parsePtrPayload();
-        const continue_expr = try p.parseWhileContinueExpr();
+        const cont_expr = try p.parseWhileContinueExpr();
 
         // TODO propose to change the syntax so that semicolons are always required
         // inside while statements, even if there is an `else`.
@@ -1098,7 +1098,7 @@ const Parser = struct {
                 return p.fail(.{ .ExpectedBlockOrAssignment = .{ .token = p.tok_i } });
             }
             if (p.eatToken(.Semicolon)) |_| {
-                if (continue_expr == 0) {
+                if (cont_expr == 0) {
                     return p.addNode(.{
                         .tag = .WhileSimple,
                         .main_token = while_token,
@@ -1114,7 +1114,7 @@ const Parser = struct {
                         .data = .{
                             .lhs = condition,
                             .rhs = try p.addExtra(Node.WhileCont{
-                                .continue_expr = continue_expr,
+                                .cont_expr = cont_expr,
                                 .then_expr = assign_expr,
                             }),
                         },
@@ -1128,7 +1128,7 @@ const Parser = struct {
             if (else_required) {
                 return p.fail(.{ .ExpectedSemiOrElse = .{ .token = p.tok_i } });
             }
-            if (continue_expr == 0) {
+            if (cont_expr == 0) {
                 return p.addNode(.{
                     .tag = .WhileSimple,
                     .main_token = while_token,
@@ -1144,7 +1144,7 @@ const Parser = struct {
                     .data = .{
                         .lhs = condition,
                         .rhs = try p.addExtra(Node.WhileCont{
-                            .continue_expr = continue_expr,
+                            .cont_expr = cont_expr,
                             .then_expr = then_expr,
                         }),
                     },
@@ -1159,7 +1159,7 @@ const Parser = struct {
             .data = .{
                 .lhs = condition,
                 .rhs = try p.addExtra(Node.While{
-                    .continue_expr = continue_expr,
+                    .cont_expr = cont_expr,
                     .then_expr = then_expr,
                     .else_expr = else_expr,
                 }),
@@ -2073,11 +2073,11 @@ const Parser = struct {
         const condition = try p.expectExpr();
         _ = try p.expectToken(.RParen);
         const then_payload = try p.parsePtrPayload();
-        const continue_expr = try p.parseWhileContinueExpr();
+        const cont_expr = try p.parseWhileContinueExpr();
 
         const then_expr = try p.expectExpr();
         const else_token = p.eatToken(.Keyword_else) orelse {
-            if (continue_expr == 0) {
+            if (cont_expr == 0) {
                 return p.addNode(.{
                     .tag = .WhileSimple,
                     .main_token = while_token,
@@ -2093,7 +2093,7 @@ const Parser = struct {
                     .data = .{
                         .lhs = condition,
                         .rhs = try p.addExtra(Node.WhileCont{
-                            .continue_expr = continue_expr,
+                            .cont_expr = cont_expr,
                             .then_expr = then_expr,
                         }),
                     },
@@ -2108,7 +2108,7 @@ const Parser = struct {
             .data = .{
                 .lhs = condition,
                 .rhs = try p.addExtra(Node.While{
-                    .continue_expr = continue_expr,
+                    .cont_expr = cont_expr,
                     .then_expr = then_expr,
                     .else_expr = else_expr,
                 }),
@@ -2836,11 +2836,11 @@ const Parser = struct {
         const condition = try p.expectExpr();
         _ = try p.expectToken(.RParen);
         const then_payload = try p.parsePtrPayload();
-        const continue_expr = try p.parseWhileContinueExpr();
+        const cont_expr = try p.parseWhileContinueExpr();
 
         const then_expr = try p.expectTypeExpr();
         const else_token = p.eatToken(.Keyword_else) orelse {
-            if (continue_expr == 0) {
+            if (cont_expr == 0) {
                 return p.addNode(.{
                     .tag = .WhileSimple,
                     .main_token = while_token,
@@ -2856,7 +2856,7 @@ const Parser = struct {
                     .data = .{
                         .lhs = condition,
                         .rhs = try p.addExtra(Node.WhileCont{
-                            .continue_expr = continue_expr,
+                            .cont_expr = cont_expr,
                             .then_expr = then_expr,
                         }),
                     },
@@ -2871,7 +2871,7 @@ const Parser = struct {
             .data = .{
                 .lhs = condition,
                 .rhs = try p.addExtra(Node.While{
-                    .continue_expr = continue_expr,
+                    .cont_expr = cont_expr,
                     .then_expr = then_expr,
                     .else_expr = else_expr,
                 }),
lib/std/zig/parser_test.zig
@@ -714,19 +714,19 @@ test "zig fmt: async function" {
 //        \\
 //    );
 //}
-//
-//test "zig fmt: while else err prong with no block" {
-//    try testCanonical(
-//        \\test "" {
-//        \\    const result = while (returnError()) |value| {
-//        \\        break value;
-//        \\    } else |err| @as(i32, 2);
-//        \\    expect(result == 2);
-//        \\}
-//        \\
-//    );
-//}
-//
+
+test "zig fmt: while else err prong with no block" {
+    try testCanonical(
+        \\test "" {
+        \\    const result = while (returnError()) |value| {
+        \\        break value;
+        \\    } else |err| @as(i32, 2);
+        \\    expect(result == 2);
+        \\}
+        \\
+    );
+}
+
 //test "zig fmt: tagged union with enum values" {
 //    try testCanonical(
 //        \\const MultipleChoice2 = union(enum(u32)) {
lib/std/zig/render.zig
@@ -567,13 +567,13 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
             if (lbrace + 1 == rbrace) {
                 // There is nothing between the braces so render condensed: `error{}`
                 try renderToken(ais, tree, lbrace, .None);
-                try renderToken(ais, tree, rbrace, space);
+                return renderToken(ais, tree, rbrace, space);
             } else if (lbrace + 2 == rbrace and token_tags[lbrace + 1] == .Identifier) {
                 // There is exactly one member and no trailing comma or
                 // comments, so render without surrounding spaces: `error{Foo}`
                 try renderToken(ais, tree, lbrace, .None);
                 try renderToken(ais, tree, lbrace + 1, .None); // identifier
-                try renderToken(ais, tree, rbrace, space);
+                return renderToken(ais, tree, rbrace, space);
             } else if (token_tags[rbrace - 1] == .Comma) {
                 // There is a trailing comma so render each member on a new line.
                 try renderToken(ais, tree, lbrace, .Newline);
@@ -589,7 +589,7 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
                     }
                 }
                 ais.popIndent();
-                try renderToken(ais, tree, rbrace, space);
+                return renderToken(ais, tree, rbrace, space);
             } else {
                 // There is no trailing comma so render everything on one line.
                 try renderToken(ais, tree, lbrace, .Space);
@@ -602,7 +602,7 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
                         else => unreachable,
                     }
                 }
-                try renderToken(ais, tree, rbrace, space);
+                return renderToken(ais, tree, rbrace, space);
             }
         },
 
@@ -663,7 +663,7 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
 
             if (cases.len == 0) {
                 try renderToken(ais, tree, rparen + 1, .None); // lbrace
-                try renderToken(ais, tree, rparen + 2, space); // rbrace
+                return renderToken(ais, tree, rparen + 2, space); // rbrace
             } else {
                 try renderToken(ais, tree, rparen + 1, .Newline); // lbrace
                 ais.pushIndent();
@@ -673,83 +673,16 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
                     try renderExpression(ais, tree, case, .Comma);
                 }
                 ais.popIndent();
-                try renderToken(ais, tree, tree.lastToken(node), space); // rbrace
+                return renderToken(ais, tree, tree.lastToken(node), space); // rbrace
             }
         },
 
-        .SwitchCaseOne => try renderSwitchCase(ais, tree, tree.switchCaseOne(node), space),
-        .SwitchCase => try renderSwitchCase(ais, tree, tree.switchCase(node), space),
+        .SwitchCaseOne => return renderSwitchCase(ais, tree, tree.switchCaseOne(node), space),
+        .SwitchCase => return renderSwitchCase(ais, tree, tree.switchCase(node), space),
 
-        .WhileSimple => unreachable, // TODO
-        .WhileCont => unreachable, // TODO
-        .While => unreachable, // TODO
-        //.While => {
-        //    const while_node = @fieldParentPtr(ast.Node.While, "base", base);
-
-        //    if (while_node.label) |label| {
-        //        try renderToken(ais, tree, label, Space.None); // label
-        //        try renderToken(ais, tree, tree.nextToken(label), Space.Space); // :
-        //    }
-
-        //    if (while_node.inline_token) |inline_token| {
-        //        try renderToken(ais, tree, inline_token, Space.Space); // inline
-        //    }
-
-        //    try renderToken(ais, tree, while_node.while_token, Space.Space); // while
-        //    try renderToken(ais, tree, tree.nextToken(while_node.while_token), Space.None); // (
-        //    try renderExpression(ais, tree, while_node.condition, Space.None);
-
-        //    const cond_rparen = tree.nextToken(while_node.condition.lastToken());
-
-        //    const body_is_block = nodeIsBlock(while_node.body);
-
-        //    var block_start_space: Space = undefined;
-        //    var after_body_space: Space = undefined;
-
-        //    if (body_is_block) {
-        //        block_start_space = Space.BlockStart;
-        //        after_body_space = if (while_node.@"else" == null) space else Space.Space;
-        //    } else if (tree.tokensOnSameLine(cond_rparen, while_node.body.lastToken())) {
-        //        block_start_space = Space.Space;
-        //        after_body_space = if (while_node.@"else" == null) space else Space.Space;
-        //    } else {
-        //        block_start_space = Space.Newline;
-        //        after_body_space = if (while_node.@"else" == null) space else Space.Newline;
-        //    }
-
-        //    {
-        //        const rparen_space = if (while_node.payload != null or while_node.continue_expr != null) Space.Space else block_start_space;
-        //        try renderToken(ais, tree, cond_rparen, rparen_space); // )
-        //    }
-
-        //    if (while_node.payload) |payload| {
-        //        const payload_space = if (while_node.continue_expr != null) Space.Space else block_start_space;
-        //        try renderExpression(ais, tree, payload, payload_space);
-        //    }
-
-        //    if (while_node.continue_expr) |continue_expr| {
-        //        const rparen = tree.nextToken(continue_expr.lastToken());
-        //        const lparen = tree.prevToken(continue_expr.firstToken());
-        //        const colon = tree.prevToken(lparen);
-
-        //        try renderToken(ais, tree, colon, Space.Space); // :
-        //        try renderToken(ais, tree, lparen, Space.None); // (
-
-        //        try renderExpression(ais, tree, continue_expr, Space.None);
-
-        //        try renderToken(ais, tree, rparen, block_start_space); // )
-        //    }
-
-        //    {
-        //        if (!body_is_block) ais.pushIndent();
-        //        defer if (!body_is_block) ais.popIndent();
-        //        try renderExpression(ais, tree, while_node.body, after_body_space);
-        //    }
-
-        //    if (while_node.@"else") |@"else"| {
-        //        return renderExpression(ais, tree, &@"else".base, space);
-        //    }
-        //},
+        .WhileSimple => return renderWhile(ais, tree, tree.whileSimple(node), space),
+        .WhileCont => return renderWhile(ais, tree, tree.whileCont(node), space),
+        .While => return renderWhile(ais, tree, tree.whileFull(node), space),
 
         .ForSimple => unreachable, // TODO
         .For => unreachable, // TODO
@@ -1092,105 +1025,142 @@ fn renderVarDecl(ais: *Ais, tree: ast.Tree, var_decl: ast.full.VarDecl) Error!vo
 }
 
 fn renderIf(ais: *Ais, tree: ast.Tree, if_node: ast.full.If, space: Space) Error!void {
+    return renderWhile(ais, tree, .{
+        .ast = .{
+            .while_token = if_node.ast.if_token,
+            .cond_expr = if_node.ast.cond_expr,
+            .cont_expr = 0,
+            .then_expr = if_node.ast.then_expr,
+            .else_expr = if_node.ast.else_expr,
+        },
+        .inline_token = null,
+        .label_token = null,
+        .payload_token = if_node.payload_token,
+        .else_token = if_node.else_token,
+        .error_token = if_node.error_token,
+    }, space);
+}
+
+/// Note that this function is additionally used to render if expressions, with
+/// respective values set to null.
+fn renderWhile(ais: *Ais, tree: ast.Tree, while_node: ast.full.While, space: Space) Error!void {
     const node_tags = tree.nodes.items(.tag);
     const token_tags = tree.tokens.items(.tag);
 
-    try renderToken(ais, tree, if_node.ast.if_token, .Space); // if
+    if (while_node.label_token) |label| {
+        try renderToken(ais, tree, label, .None); // label
+        try renderToken(ais, tree, label + 1, .Space); // :
+    }
 
-    const lparen = if_node.ast.if_token + 1;
+    if (while_node.inline_token) |inline_token| {
+        try renderToken(ais, tree, inline_token, .Space); // inline
+    }
 
-    try renderToken(ais, tree, lparen, .None); // (
-    try renderExpression(ais, tree, if_node.ast.cond_expr, .None); // condition
+    try renderToken(ais, tree, while_node.ast.while_token, .Space); // if
+    try renderToken(ais, tree, while_node.ast.while_token + 1, .None); // (
+    try renderExpression(ais, tree, while_node.ast.cond_expr, .None); // condition
 
-    switch (node_tags[if_node.ast.then_expr]) {
-        .If, .IfSimple => {
-            try renderExtraNewline(ais, tree, if_node.ast.then_expr);
-        },
-        .Block, .For, .ForSimple, .While, .WhileSimple, .Switch => {
-            if (if_node.payload_token) |payload_token| {
-                try renderToken(ais, tree, payload_token - 2, .Space); // )
-                try renderToken(ais, tree, payload_token - 1, .None); // |
-                if (token_tags[payload_token] == .Asterisk) {
-                    try renderToken(ais, tree, payload_token, .None); // *
-                    try renderToken(ais, tree, payload_token + 1, .None); // identifier
-                    try renderToken(ais, tree, payload_token + 2, .BlockStart); // |
-                } else {
-                    try renderToken(ais, tree, payload_token, .None); // identifier
-                    try renderToken(ais, tree, payload_token + 1, .BlockStart); // |
-                }
+    if (nodeIsBlock(node_tags[while_node.ast.then_expr])) {
+        const payload_space: Space = if (while_node.ast.cont_expr != 0) .Space else .BlockStart;
+        if (while_node.payload_token) |payload_token| {
+            try renderToken(ais, tree, payload_token - 2, .Space); // )
+            try renderToken(ais, tree, payload_token - 1, .None); // |
+            if (token_tags[payload_token] == .Asterisk) {
+                try renderToken(ais, tree, payload_token, .None); // *
+                try renderToken(ais, tree, payload_token + 1, .None); // identifier
+                try renderToken(ais, tree, payload_token + 2, payload_space); // |
             } else {
-                const rparen = tree.lastToken(if_node.ast.cond_expr) + 1;
-                try renderToken(ais, tree, rparen, .BlockStart); // )
+                try renderToken(ais, tree, payload_token, .None); // identifier
+                try renderToken(ais, tree, payload_token + 1, payload_space); // |
             }
-            if (if_node.ast.else_expr != 0) {
-                try renderExpression(ais, tree, if_node.ast.then_expr, Space.Space);
-                try renderToken(ais, tree, if_node.else_token, .Space); // else
-                if (if_node.error_token) |error_token| {
-                    try renderToken(ais, tree, error_token - 1, .None); // |
-                    try renderToken(ais, tree, error_token, .None); // identifier
-                    try renderToken(ais, tree, error_token + 1, .Space); // |
-                }
-                return renderExpression(ais, tree, if_node.ast.else_expr, space);
-            } else {
-                return renderExpression(ais, tree, if_node.ast.then_expr, space);
+        } else {
+            const rparen = tree.lastToken(while_node.ast.cond_expr) + 1;
+            try renderToken(ais, tree, rparen, payload_space); // )
+        }
+        if (while_node.ast.cont_expr != 0) {
+            const rparen = tree.lastToken(while_node.ast.cont_expr) + 1;
+            const lparen = tree.firstToken(while_node.ast.cont_expr) - 1;
+            try renderToken(ais, tree, lparen - 1, .Space); // :
+            try renderToken(ais, tree, lparen, .None); // lparen
+            try renderExpression(ais, tree, while_node.ast.cont_expr, .None);
+            try renderToken(ais, tree, rparen, .BlockStart); // rparen
+        }
+        if (while_node.ast.else_expr != 0) {
+            try renderExpression(ais, tree, while_node.ast.then_expr, Space.Space);
+            try renderToken(ais, tree, while_node.else_token, .Space); // else
+            if (while_node.error_token) |error_token| {
+                try renderToken(ais, tree, error_token - 1, .None); // |
+                try renderToken(ais, tree, error_token, .None); // identifier
+                try renderToken(ais, tree, error_token + 1, .Space); // |
             }
-        },
-        else => {},
+            return renderExpression(ais, tree, while_node.ast.else_expr, space);
+        } else {
+            return renderExpression(ais, tree, while_node.ast.then_expr, space);
+        }
     }
 
-    const rparen = tree.lastToken(if_node.ast.cond_expr) + 1;
-    const last_then_token = tree.lastToken(if_node.ast.then_expr);
+    const rparen = tree.lastToken(while_node.ast.cond_expr) + 1;
+    const last_then_token = tree.lastToken(while_node.ast.then_expr);
     const src_has_newline = !tree.tokensOnSameLine(rparen, last_then_token);
 
     if (src_has_newline) {
-        if (if_node.payload_token) |payload_token| {
+        const payload_space: Space = if (while_node.ast.cont_expr != 0) .Space else .Newline;
+        if (while_node.payload_token) |payload_token| {
             try renderToken(ais, tree, payload_token - 2, .Space); // )
             try renderToken(ais, tree, payload_token - 1, .None); // |
             try renderToken(ais, tree, payload_token, .None); // identifier
-            try renderToken(ais, tree, payload_token + 1, .Newline); // |
+            try renderToken(ais, tree, payload_token + 1, payload_space); // |
         } else {
             ais.pushIndent();
-            try renderToken(ais, tree, rparen, .Newline); // )
+            try renderToken(ais, tree, rparen, payload_space); // )
             ais.popIndent();
         }
-        if (if_node.ast.else_expr != 0) {
+        if (while_node.ast.cont_expr != 0) {
+            const cont_rparen = tree.lastToken(while_node.ast.cont_expr) + 1;
+            const cont_lparen = tree.firstToken(while_node.ast.cont_expr) - 1;
+            try renderToken(ais, tree, cont_lparen - 1, .Space); // :
+            try renderToken(ais, tree, cont_lparen, .None); // lparen
+            try renderExpression(ais, tree, while_node.ast.cont_expr, .None);
+            try renderToken(ais, tree, cont_rparen, .Newline); // rparen
+        }
+        if (while_node.ast.else_expr != 0) {
             ais.pushIndent();
-            try renderExpression(ais, tree, if_node.ast.then_expr, Space.Newline);
+            try renderExpression(ais, tree, while_node.ast.then_expr, Space.Newline);
             ais.popIndent();
-            const else_is_block = nodeIsBlock(node_tags[if_node.ast.else_expr]);
+            const else_is_block = nodeIsBlock(node_tags[while_node.ast.else_expr]);
             if (else_is_block) {
-                try renderToken(ais, tree, if_node.else_token, .Space); // else
-                if (if_node.error_token) |error_token| {
+                try renderToken(ais, tree, while_node.else_token, .Space); // else
+                if (while_node.error_token) |error_token| {
                     try renderToken(ais, tree, error_token - 1, .None); // |
                     try renderToken(ais, tree, error_token, .None); // identifier
                     try renderToken(ais, tree, error_token + 1, .Space); // |
                 }
-                return renderExpression(ais, tree, if_node.ast.else_expr, space);
+                return renderExpression(ais, tree, while_node.ast.else_expr, space);
             } else {
-                if (if_node.error_token) |error_token| {
-                    try renderToken(ais, tree, if_node.else_token, .Space); // else
+                if (while_node.error_token) |error_token| {
+                    try renderToken(ais, tree, while_node.else_token, .Space); // else
                     try renderToken(ais, tree, error_token - 1, .None); // |
                     try renderToken(ais, tree, error_token, .None); // identifier
                     try renderToken(ais, tree, error_token + 1, .Space); // |
                 } else {
-                    try renderToken(ais, tree, if_node.else_token, .Newline); // else
+                    try renderToken(ais, tree, while_node.else_token, .Newline); // else
                 }
                 ais.pushIndent();
-                try renderExpression(ais, tree, if_node.ast.else_expr, space);
+                try renderExpression(ais, tree, while_node.ast.else_expr, space);
                 ais.popIndent();
                 return;
             }
         } else {
             ais.pushIndent();
-            try renderExpression(ais, tree, if_node.ast.then_expr, space);
+            try renderExpression(ais, tree, while_node.ast.then_expr, space);
             ais.popIndent();
             return;
         }
     }
 
-    // Single line if statement.
+    // Render everything on a single line.
 
-    if (if_node.payload_token) |payload_token| {
+    if (while_node.payload_token) |payload_token| {
         assert(payload_token - 2 == rparen);
         try renderToken(ais, tree, payload_token - 2, .Space); // )
         try renderToken(ais, tree, payload_token - 1, .None); // |
@@ -1206,19 +1176,28 @@ fn renderIf(ais: *Ais, tree: ast.Tree, if_node: ast.full.If, space: Space) Error
         try renderToken(ais, tree, rparen, .Space); // )
     }
 
-    if (if_node.ast.else_expr != 0) {
-        try renderExpression(ais, tree, if_node.ast.then_expr, .Space);
-        try renderToken(ais, tree, if_node.else_token, .Space); // else
+    if (while_node.ast.cont_expr != 0) {
+        const cont_rparen = tree.lastToken(while_node.ast.cont_expr) + 1;
+        const cont_lparen = tree.firstToken(while_node.ast.cont_expr) - 1;
+        try renderToken(ais, tree, cont_lparen - 1, .Space); // :
+        try renderToken(ais, tree, cont_lparen, .None); // lparen
+        try renderExpression(ais, tree, while_node.ast.cont_expr, .None);
+        try renderToken(ais, tree, cont_rparen, .Space); // rparen
+    }
+
+    if (while_node.ast.else_expr != 0) {
+        try renderExpression(ais, tree, while_node.ast.then_expr, .Space);
+        try renderToken(ais, tree, while_node.else_token, .Space); // else
 
-        if (if_node.error_token) |error_token| {
+        if (while_node.error_token) |error_token| {
             try renderToken(ais, tree, error_token - 1, .None); // |
             try renderToken(ais, tree, error_token, .None); // identifier
             try renderToken(ais, tree, error_token + 1, .Space); // |
         }
 
-        return renderExpression(ais, tree, if_node.ast.else_expr, space);
+        return renderExpression(ais, tree, while_node.ast.else_expr, space);
     } else {
-        return renderExpression(ais, tree, if_node.ast.then_expr, space);
+        return renderExpression(ais, tree, while_node.ast.then_expr, space);
     }
 }
 
@@ -2079,12 +2058,16 @@ fn renderDocComments(ais: *Ais, tree: ast.Tree, end_token: ast.TokenIndex) Error
 fn nodeIsBlock(tag: ast.Node.Tag) bool {
     return switch (tag) {
         .Block,
+        .BlockSemicolon,
+        .BlockTwo,
+        .BlockTwoSemicolon,
         .If,
         .IfSimple,
         .For,
         .ForSimple,
         .While,
         .WhileSimple,
+        .WhileCont,
         .Switch,
         => true,
         else => false,