Commit 1b7055b514

Veikka Tuominen <git@vexu.eu>
2023-01-30 18:10:03
parse and render new for loop syntax
1 parent 5e7b09c
Changed files (4)
lib/std/zig/Ast.zig
@@ -386,6 +386,12 @@ pub fn renderError(tree: Ast, parse_error: Error, stream: anytype) !void {
         .expected_comma_after_switch_prong => {
             return stream.writeAll("expected ',' after switch prong");
         },
+        .expected_comma_after_for_operand => {
+            return stream.writeAll("expected ',' after for operand");
+        },
+        .expected_comma_after_capture => {
+            return stream.writeAll("expected ',' after for capture");
+        },
         .expected_initializer => {
             return stream.writeAll("expected field initializer");
         },
@@ -420,6 +426,12 @@ pub fn renderError(tree: Ast, parse_error: Error, stream: anytype) !void {
         .var_const_decl => {
             return stream.writeAll("use 'var' or 'const' to declare variable");
         },
+        .extra_for_capture => {
+            return stream.writeAll("excess for captures");
+        },
+        .for_input_not_captured => {
+            return stream.writeAll("for input is not captured");
+        },
 
         .expected_token => {
             const found_tag = token_tags[parse_error.token + @boolToInt(parse_error.token_is_prev)];
@@ -568,6 +580,7 @@ pub fn firstToken(tree: Ast, node: Node.Index) TokenIndex {
         .call,
         .call_comma,
         .switch_range,
+        .for_range,
         .error_union,
         => n = datas[n].lhs,
 
@@ -845,6 +858,12 @@ pub fn lastToken(tree: Ast, node: Node.Index) TokenIndex {
         .switch_range,
         => n = datas[n].rhs,
 
+        .for_range => if (datas[n].rhs != 0) {
+            n = datas[n].rhs;
+        } else {
+            return main_tokens[n] + end_offset;
+        },
+
         .field_access,
         .unwrap_optional,
         .grouped_expression,
@@ -1263,11 +1282,15 @@ pub fn lastToken(tree: Ast, node: Node.Index) TokenIndex {
             assert(extra.else_expr != 0);
             n = extra.else_expr;
         },
-        .@"if", .@"for" => {
+        .@"if" => {
             const extra = tree.extraData(datas[n].rhs, Node.If);
             assert(extra.else_expr != 0);
             n = extra.else_expr;
         },
+        .@"for" => {
+            const extra = @bitCast(Node.For, datas[n].rhs);
+            n = tree.extra_data[datas[n].lhs + extra.inputs + @boolToInt(extra.has_else)];
+        },
         .@"suspend" => {
             if (datas[n].lhs != 0) {
                 n = datas[n].lhs;
@@ -1916,26 +1939,28 @@ pub fn whileFull(tree: Ast, node: Node.Index) full.While {
     });
 }
 
-pub fn forSimple(tree: Ast, node: Node.Index) full.While {
-    const data = tree.nodes.items(.data)[node];
-    return tree.fullWhileComponents(.{
-        .while_token = tree.nodes.items(.main_token)[node],
-        .cond_expr = data.lhs,
-        .cont_expr = 0,
+pub fn forSimple(tree: Ast, node: Node.Index) full.For {
+    const data = &tree.nodes.items(.data)[node];
+    const inputs: *[1]Node.Index = &data.lhs;
+    return tree.fullForComponents(.{
+        .for_token = tree.nodes.items(.main_token)[node],
+        .inputs = inputs[0..1],
         .then_expr = data.rhs,
         .else_expr = 0,
     });
 }
 
-pub fn forFull(tree: Ast, node: Node.Index) full.While {
+pub fn forFull(tree: Ast, node: Node.Index) full.For {
     const data = tree.nodes.items(.data)[node];
-    const extra = tree.extraData(data.rhs, Node.If);
-    return tree.fullWhileComponents(.{
-        .while_token = tree.nodes.items(.main_token)[node],
-        .cond_expr = data.lhs,
-        .cont_expr = 0,
-        .then_expr = extra.then_expr,
-        .else_expr = extra.else_expr,
+    const extra = @bitCast(Node.For, data.rhs);
+    const inputs = tree.extra_data[data.lhs..][0..extra.inputs];
+    const then_expr = tree.extra_data[data.lhs + extra.inputs];
+    const else_expr = if (extra.has_else) tree.extra_data[data.lhs + extra.inputs + 1] else 0;
+    return tree.fullForComponents(.{
+        .for_token = tree.nodes.items(.main_token)[node],
+        .inputs = inputs,
+        .then_expr = then_expr,
+        .else_expr = else_expr,
     });
 }
 
@@ -2243,6 +2268,33 @@ fn fullWhileComponents(tree: Ast, info: full.While.Components) full.While {
     return result;
 }
 
+fn fullForComponents(tree: Ast, info: full.For.Components) full.For {
+    const token_tags = tree.tokens.items(.tag);
+    var result: full.For = .{
+        .ast = info,
+        .inline_token = null,
+        .label_token = null,
+        .payload_token = undefined,
+        .else_token = undefined,
+    };
+    var tok_i = info.for_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.inputs[info.inputs.len - 1]);
+    result.payload_token = last_cond_token + 3 + @boolToInt(token_tags[last_cond_token + 1] == .comma);
+    if (info.else_expr != 0) {
+        result.else_token = tree.lastToken(info.then_expr) + 1;
+    }
+    return result;
+}
+
 fn fullCallComponents(tree: Ast, info: full.Call.Components) full.Call {
     const token_tags = tree.tokens.items(.tag);
     var result: full.Call = .{
@@ -2279,6 +2331,12 @@ pub fn fullWhile(tree: Ast, node: Node.Index) ?full.While {
         .while_simple => tree.whileSimple(node),
         .while_cont => tree.whileCont(node),
         .@"while" => tree.whileFull(node),
+        else => null,
+    };
+}
+
+pub fn fullFor(tree: Ast, node: Node.Index) ?full.For {
+    return switch (tree.nodes.items(.tag)[node]) {
         .for_simple => tree.forSimple(node),
         .@"for" => tree.forFull(node),
         else => null,
@@ -2453,6 +2511,22 @@ pub const full = struct {
         };
     };
 
+    pub const For = struct {
+        ast: Components,
+        inline_token: ?TokenIndex,
+        label_token: ?TokenIndex,
+        payload_token: TokenIndex,
+        /// Populated only if else_expr != 0.
+        else_token: TokenIndex,
+
+        pub const Components = struct {
+            for_token: TokenIndex,
+            inputs: []const Node.Index,
+            then_expr: Node.Index,
+            else_expr: Node.Index,
+        };
+    };
+
     pub const ContainerField = struct {
         comptime_token: ?TokenIndex,
         ast: Components,
@@ -2795,6 +2869,8 @@ pub const Error = struct {
         expected_comma_after_param,
         expected_comma_after_initializer,
         expected_comma_after_switch_prong,
+        expected_comma_after_for_operand,
+        expected_comma_after_capture,
         expected_initializer,
         mismatched_binary_op_whitespace,
         invalid_ampersand_ampersand,
@@ -2802,6 +2878,8 @@ pub const Error = struct {
         expected_var_const,
         wrong_equal_var_decl,
         var_const_decl,
+        extra_for_capture,
+        for_input_not_captured,
 
         zig_style_container,
         previous_field,
@@ -3112,8 +3190,10 @@ pub const Node = struct {
         @"while",
         /// `for (lhs) rhs`.
         for_simple,
-        /// `for (lhs) a else b`. `if_list[rhs]`.
+        /// `for (lhs[0..inputs]) lhs[inputs + 1] else lhs[inputs + 2]`. `For[rhs]`.
         @"for",
+        /// `lhs..rhs`.
+        for_range,
         /// `if (lhs) rhs`.
         /// `if (lhs) |a| rhs`.
         if_simple,
@@ -3369,6 +3449,11 @@ pub const Node = struct {
         then_expr: Index,
     };
 
+    pub const For = packed struct(u32) {
+        inputs: u31,
+        has_else: bool,
+    };
+
     pub const FnProtoOne = struct {
         /// Populated if there is exactly 1 parameter. Otherwise there are 0 parameters.
         param: Index,
lib/std/zig/Parse.zig
@@ -104,6 +104,8 @@ fn warnMsg(p: *Parse, msg: Ast.Error) error{OutOfMemory}!void {
         .expected_comma_after_param,
         .expected_comma_after_initializer,
         .expected_comma_after_switch_prong,
+        .expected_comma_after_for_operand,
+        .expected_comma_after_capture,
         .expected_semi_or_else,
         .expected_semi_or_lbrace,
         .expected_token,
@@ -1149,22 +1151,18 @@ fn parseLoopStatement(p: *Parse) !Node.Index {
     return p.fail(.expected_inlinable);
 }
 
-/// ForPrefix <- KEYWORD_for LPAREN Expr RPAREN PtrIndexPayload
-///
 /// ForStatement
 ///     <- ForPrefix BlockExpr ( KEYWORD_else Statement )?
 ///      / ForPrefix AssignExpr ( SEMICOLON / KEYWORD_else Statement )
 fn parseForStatement(p: *Parse) !Node.Index {
     const for_token = p.eatToken(.keyword_for) orelse return null_node;
-    _ = try p.expectToken(.l_paren);
-    const array_expr = try p.expectExpr();
-    _ = try p.expectToken(.r_paren);
-    const found_payload = try p.parsePtrIndexPayload();
-    if (found_payload == 0) try p.warn(.expected_loop_payload);
 
-    // TODO propose to change the syntax so that semicolons are always required
-    // inside while statements, even if there is an `else`.
+    const scratch_top = p.scratch.items.len;
+    defer p.scratch.shrinkRetainingCapacity(scratch_top);
+    const inputs = try p.forPrefix();
+
     var else_required = false;
+    var seen_semicolon = false;
     const then_expr = blk: {
         const block_expr = try p.parseBlockExpr();
         if (block_expr != 0) break :blk block_expr;
@@ -1173,39 +1171,40 @@ fn parseForStatement(p: *Parse) !Node.Index {
             return p.fail(.expected_block_or_assignment);
         }
         if (p.eatToken(.semicolon)) |_| {
-            return p.addNode(.{
-                .tag = .for_simple,
-                .main_token = for_token,
-                .data = .{
-                    .lhs = array_expr,
-                    .rhs = assign_expr,
-                },
-            });
+            seen_semicolon = true;
+            break :blk assign_expr;
         }
         else_required = true;
         break :blk assign_expr;
     };
-    _ = p.eatToken(.keyword_else) orelse {
-        if (else_required) {
-            try p.warn(.expected_semi_or_else);
-        }
+    var has_else = false;
+    if (!seen_semicolon and p.eatToken(.keyword_else) != null) {
+        try p.scratch.append(p.gpa, then_expr);
+        const else_stmt = try p.expectStatement(false);
+        try p.scratch.append(p.gpa, else_stmt);
+        has_else = true;
+    } else if (inputs == 1) {
+        if (else_required) try p.warn(.expected_semi_or_else);
         return p.addNode(.{
             .tag = .for_simple,
             .main_token = for_token,
             .data = .{
-                .lhs = array_expr,
+                .lhs = p.scratch.items[scratch_top],
                 .rhs = then_expr,
             },
         });
-    };
+    } else {
+        if (else_required) try p.warn(.expected_semi_or_else);
+        try p.scratch.append(p.gpa, then_expr);
+    }
     return p.addNode(.{
         .tag = .@"for",
         .main_token = for_token,
         .data = .{
-            .lhs = array_expr,
-            .rhs = try p.addExtra(Node.If{
-                .then_expr = then_expr,
-                .else_expr = try p.expectStatement(false),
+            .lhs = (try p.listToSpan(p.scratch.items[scratch_top..])).start,
+            .rhs = @bitCast(u32, Node.For{
+                .inputs = @intCast(u31, inputs),
+                .has_else = has_else,
             }),
         },
     });
@@ -2056,42 +2055,118 @@ fn parseBlock(p: *Parse) !Node.Index {
     }
 }
 
-/// ForPrefix <- KEYWORD_for LPAREN Expr RPAREN PtrIndexPayload
-///
 /// ForExpr <- ForPrefix Expr (KEYWORD_else Expr)?
 fn parseForExpr(p: *Parse) !Node.Index {
     const for_token = p.eatToken(.keyword_for) orelse return null_node;
-    _ = try p.expectToken(.l_paren);
-    const array_expr = try p.expectExpr();
-    _ = try p.expectToken(.r_paren);
-    const found_payload = try p.parsePtrIndexPayload();
-    if (found_payload == 0) try p.warn(.expected_loop_payload);
+
+    const scratch_top = p.scratch.items.len;
+    defer p.scratch.shrinkRetainingCapacity(scratch_top);
+    const inputs = try p.forPrefix();
 
     const then_expr = try p.expectExpr();
-    _ = p.eatToken(.keyword_else) orelse {
+    var has_else = false;
+    if (p.eatToken(.keyword_else)) |_| {
+        try p.scratch.append(p.gpa, then_expr);
+        const else_expr = try p.expectExpr();
+        try p.scratch.append(p.gpa, else_expr);
+        has_else = true;
+    } else if (inputs == 1) {
         return p.addNode(.{
             .tag = .for_simple,
             .main_token = for_token,
             .data = .{
-                .lhs = array_expr,
+                .lhs = p.scratch.items[scratch_top],
                 .rhs = then_expr,
             },
         });
-    };
-    const else_expr = try p.expectExpr();
+    } else {
+        try p.scratch.append(p.gpa, then_expr);
+    }
     return p.addNode(.{
         .tag = .@"for",
         .main_token = for_token,
         .data = .{
-            .lhs = array_expr,
-            .rhs = try p.addExtra(Node.If{
-                .then_expr = then_expr,
-                .else_expr = else_expr,
+            .lhs = (try p.listToSpan(p.scratch.items[scratch_top..])).start,
+            .rhs = @bitCast(u32, Node.For{
+                .inputs = @intCast(u31, inputs),
+                .has_else = has_else,
             }),
         },
     });
 }
 
+/// ForPrefix <- KEYWORD_for LPAREN ForInput (COMMA ForInput)* COMMA? RPAREN ForPayload
+///
+/// ForInput <- Expr (DOT2 Expr?)?
+///
+/// ForPayload <- PIPE ASTERISK? IDENTIFIER (COMMA ASTERISK? IDENTIFIER)* PIPE
+fn forPrefix(p: *Parse) Error!usize {
+    const start = p.scratch.items.len;
+    _ = try p.expectToken(.l_paren);
+
+    while (true) {
+        var input = try p.expectExpr();
+        if (p.eatToken(.ellipsis2)) |ellipsis| {
+            input = try p.addNode(.{
+                .tag = .for_range,
+                .main_token = ellipsis,
+                .data = .{
+                    .lhs = input,
+                    .rhs = try p.parseExpr(),
+                },
+            });
+        }
+
+        try p.scratch.append(p.gpa, input);
+        switch (p.token_tags[p.tok_i]) {
+            .comma => p.tok_i += 1,
+            .r_paren => {
+                p.tok_i += 1;
+                break;
+            },
+            .colon, .r_brace, .r_bracket => return p.failExpected(.r_paren),
+            // Likely just a missing comma; give error but continue parsing.
+            else => try p.warn(.expected_comma_after_for_operand),
+        }
+        if (p.eatToken(.r_paren)) |_| break;
+    }
+    const inputs = p.scratch.items.len - start;
+
+    _ = p.eatToken(.pipe) orelse {
+        try p.warn(.expected_loop_payload);
+        return inputs;
+    };
+
+    var warned_excess = false;
+    var captures: u32 = 0;
+    while (true) {
+        _ = p.eatToken(.asterisk);
+        const identifier = try p.expectToken(.identifier);
+        captures += 1;
+        if (captures > inputs and !warned_excess) {
+            try p.warnMsg(.{ .tag = .extra_for_capture, .token = identifier });
+            warned_excess = true;
+        }
+        switch (p.token_tags[p.tok_i]) {
+            .comma => p.tok_i += 1,
+            .pipe => {
+                p.tok_i += 1;
+                break;
+            },
+            // Likely just a missing comma; give error but continue parsing.
+            else => try p.warn(.expected_comma_after_capture),
+        }
+        if (p.eatToken(.pipe)) |_| break;
+    }
+
+    if (captures < inputs) {
+        const index = p.scratch.items.len - captures;
+        const input = p.nodes.items(.main_token)[p.scratch.items[index]];
+        try p.warnMsg(.{ .tag = .for_input_not_captured, .token = input });
+    }
+    return inputs;
+}
+
 /// WhilePrefix <- KEYWORD_while LPAREN Expr RPAREN PtrPayload? WhileContinueExpr?
 ///
 /// WhileExpr <- WhilePrefix Expr (KEYWORD_else Payload? Expr)?
@@ -2752,37 +2827,41 @@ fn expectPrimaryTypeExpr(p: *Parse) !Node.Index {
     return node;
 }
 
-/// ForPrefix <- KEYWORD_for LPAREN Expr RPAREN PtrIndexPayload
-///
 /// ForTypeExpr <- ForPrefix TypeExpr (KEYWORD_else TypeExpr)?
 fn parseForTypeExpr(p: *Parse) !Node.Index {
     const for_token = p.eatToken(.keyword_for) orelse return null_node;
-    _ = try p.expectToken(.l_paren);
-    const array_expr = try p.expectExpr();
-    _ = try p.expectToken(.r_paren);
-    const found_payload = try p.parsePtrIndexPayload();
-    if (found_payload == 0) try p.warn(.expected_loop_payload);
+
+    const scratch_top = p.scratch.items.len;
+    defer p.scratch.shrinkRetainingCapacity(scratch_top);
+    const inputs = try p.forPrefix();
 
     const then_expr = try p.expectTypeExpr();
-    _ = p.eatToken(.keyword_else) orelse {
+    var has_else = false;
+    if (p.eatToken(.keyword_else)) |_| {
+        try p.scratch.append(p.gpa, then_expr);
+        const else_expr = try p.expectTypeExpr();
+        try p.scratch.append(p.gpa, else_expr);
+        has_else = true;
+    } else if (inputs == 1) {
         return p.addNode(.{
             .tag = .for_simple,
             .main_token = for_token,
             .data = .{
-                .lhs = array_expr,
+                .lhs = p.scratch.items[scratch_top],
                 .rhs = then_expr,
             },
         });
-    };
-    const else_expr = try p.expectTypeExpr();
+    } else {
+        try p.scratch.append(p.gpa, then_expr);
+    }
     return p.addNode(.{
         .tag = .@"for",
         .main_token = for_token,
         .data = .{
-            .lhs = array_expr,
-            .rhs = try p.addExtra(Node.If{
-                .then_expr = then_expr,
-                .else_expr = else_expr,
+            .lhs = (try p.listToSpan(p.scratch.items[scratch_top..])).start,
+            .rhs = @bitCast(u32, Node.For{
+                .inputs = @intCast(u31, inputs),
+                .has_else = has_else,
             }),
         },
     });
lib/std/zig/parser_test.zig
@@ -3457,11 +3457,11 @@ test "zig fmt: for" {
         \\    for (a) |*v|
         \\        continue;
         \\
-        \\    for (a) |v, i| {
+        \\    for (a, 0..) |v, i| {
         \\        continue;
         \\    }
         \\
-        \\    for (a) |v, i|
+        \\    for (a, 0..) |v, i|
         \\        continue;
         \\
         \\    for (a) |b| switch (b) {
@@ -3469,17 +3469,24 @@ test "zig fmt: for" {
         \\        d => {},
         \\    };
         \\
-        \\    const res = for (a) |v, i| {
+        \\    const res = for (a, 0..) |v, i| {
         \\        break v;
         \\    } else {
         \\        unreachable;
         \\    };
         \\
         \\    var num: usize = 0;
-        \\    inline for (a) |v, i| {
+        \\    inline for (a, 0..1) |v, i| {
         \\        num += v;
         \\        num += i;
         \\    }
+        \\
+        \\    for (a, b) |
+        \\        long_name,
+        \\        another_long_name,
+        \\    | {
+        \\        continue;
+        \\    }
         \\}
         \\
     );
@@ -3499,6 +3506,26 @@ test "zig fmt: for" {
         \\}
         \\
     );
+
+    try testTransform(
+        \\test "fix for" {
+        \\    for (a, b, c,) |long, another, third,| {}
+        \\}
+        \\
+    ,
+        \\test "fix for" {
+        \\    for (
+        \\        a,
+        \\        b,
+        \\        c,
+        \\    ) |
+        \\        long,
+        \\        another,
+        \\        third,
+        \\    | {}
+        \\}
+        \\
+    );
 }
 
 test "zig fmt: for if" {
@@ -4358,7 +4385,7 @@ test "zig fmt: hex literals with underscore separators" {
     try testTransform(
         \\pub fn orMask(a: [ 1_000 ]u64, b: [  1_000]  u64) [1_000]u64 {
         \\    var c: [1_000]u64 =  [1]u64{ 0xFFFF_FFFF_FFFF_FFFF}**1_000;
-        \\    for (c [ 1_0 .. ]) |_, i| {
+        \\    for (c [ 1_0 .. ], 0..) |_, i| {
         \\        c[i] = (a[i] | b[i]) & 0xCCAA_CCAA_CCAA_CCAA;
         \\    }
         \\    return c;
@@ -4368,7 +4395,7 @@ test "zig fmt: hex literals with underscore separators" {
     ,
         \\pub fn orMask(a: [1_000]u64, b: [1_000]u64) [1_000]u64 {
         \\    var c: [1_000]u64 = [1]u64{0xFFFF_FFFF_FFFF_FFFF} ** 1_000;
-        \\    for (c[1_0..]) |_, i| {
+        \\    for (c[1_0..], 0..) |_, i| {
         \\        c[i] = (a[i] | b[i]) & 0xCCAA_CCAA_CCAA_CCAA;
         \\    }
         \\    return c;
@@ -4880,10 +4907,10 @@ test "zig fmt: remove trailing whitespace after doc comment" {
 test "zig fmt: for loop with ptr payload and index" {
     try testCanonical(
         \\test {
-        \\    for (self.entries.items) |*item, i| {}
-        \\    for (self.entries.items) |*item, i|
+        \\    for (self.entries.items, 0..) |*item, i| {}
+        \\    for (self.entries.items, 0..) |*item, i|
         \\        a = b;
-        \\    for (self.entries.items) |*item, i| a = b;
+        \\    for (self.entries.items, 0..) |*item, i| a = b;
         \\}
         \\
     );
@@ -5471,7 +5498,7 @@ test "zig fmt: canonicalize symbols (primitive types)" {
         \\    _ = @"void": {
         \\        break :@"void";
         \\    };
-        \\    for ("hi") |@"u3", @"i4"| {
+        \\    for ("hi", 0..) |@"u3", @"i4"| {
         \\        _ = @"u3";
         \\        _ = @"i4";
         \\    }
@@ -5523,7 +5550,7 @@ test "zig fmt: canonicalize symbols (primitive types)" {
         \\    _ = void: {
         \\        break :void;
         \\    };
-        \\    for ("hi") |@"u3", @"i4"| {
+        \\    for ("hi", 0..) |@"u3", @"i4"| {
         \\        _ = @"u3";
         \\        _ = @"i4";
         \\    }
lib/std/zig/render.zig
@@ -353,6 +353,16 @@ fn renderExpression(gpa: Allocator, ais: *Ais, tree: Ast, node: Ast.Node.Index,
             try renderToken(ais, tree, main_tokens[node], .none);
             return renderExpression(gpa, ais, tree, infix.rhs, space);
         },
+        .for_range => {
+            const infix = datas[node];
+            try renderExpression(gpa, ais, tree, infix.lhs, .none);
+            if (infix.rhs != 0) {
+                try renderToken(ais, tree, main_tokens[node], .none);
+                return renderExpression(gpa, ais, tree, infix.rhs, space);
+            } else {
+                return renderToken(ais, tree, main_tokens[node], space);
+            }
+        },
 
         .add,
         .add_wrap,
@@ -694,9 +704,11 @@ fn renderExpression(gpa: Allocator, ais: *Ais, tree: Ast, node: Ast.Node.Index,
         .while_simple,
         .while_cont,
         .@"while",
+        => return renderWhile(gpa, ais, tree, tree.fullWhile(node).?, space),
+
         .for_simple,
         .@"for",
-        => return renderWhile(gpa, ais, tree, tree.fullWhile(node).?, space),
+        => return renderFor(gpa, ais, tree, tree.fullFor(node).?, space),
 
         .if_simple,
         .@"if",
@@ -1054,10 +1066,9 @@ fn renderIf(gpa: Allocator, ais: *Ais, tree: Ast, if_node: Ast.full.If, space: S
     }, space);
 }
 
-/// Note that this function is additionally used to render if and for expressions, with
+/// Note that this function is additionally used to render if expressions, with
 /// respective values set to null.
 fn renderWhile(gpa: Allocator, ais: *Ais, tree: Ast, while_node: Ast.full.While, space: Space) Error!void {
-    const node_tags = tree.nodes.items(.tag);
     const token_tags = tree.tokens.items(.tag);
 
     if (while_node.label_token) |label| {
@@ -1108,9 +1119,34 @@ fn renderWhile(gpa: Allocator, ais: *Ais, tree: Ast, while_node: Ast.full.While,
         last_prefix_token = tree.lastToken(while_node.ast.cont_expr) + 1; // rparen
     }
 
-    const then_expr_is_block = nodeIsBlock(node_tags[while_node.ast.then_expr]);
+    try renderThenElse(
+        gpa,
+        ais,
+        tree,
+        last_prefix_token,
+        while_node.ast.then_expr,
+        while_node.else_token,
+        while_node.error_token,
+        while_node.ast.else_expr,
+        space,
+    );
+}
+
+fn renderThenElse(
+    gpa: Allocator,
+    ais: *Ais,
+    tree: Ast,
+    last_prefix_token: Ast.TokenIndex,
+    then_expr: Ast.Node.Index,
+    else_token: Ast.TokenIndex,
+    maybe_error_token: ?Ast.TokenIndex,
+    else_expr: Ast.Node.Index,
+    space: Space,
+) Error!void {
+    const node_tags = tree.nodes.items(.tag);
+    const then_expr_is_block = nodeIsBlock(node_tags[then_expr]);
     const indent_then_expr = !then_expr_is_block and
-        !tree.tokensOnSameLine(last_prefix_token, tree.firstToken(while_node.ast.then_expr));
+        !tree.tokensOnSameLine(last_prefix_token, tree.firstToken(then_expr));
     if (indent_then_expr or (then_expr_is_block and ais.isLineOverIndented())) {
         ais.pushIndentNextLine();
         try renderToken(ais, tree, last_prefix_token, .newline);
@@ -1119,43 +1155,113 @@ fn renderWhile(gpa: Allocator, ais: *Ais, tree: Ast, while_node: Ast.full.While,
         try renderToken(ais, tree, last_prefix_token, .space);
     }
 
-    if (while_node.ast.else_expr != 0) {
+    if (else_expr != 0) {
         if (indent_then_expr) {
             ais.pushIndent();
-            try renderExpression(gpa, ais, tree, while_node.ast.then_expr, .newline);
+            try renderExpression(gpa, ais, tree, then_expr, .newline);
             ais.popIndent();
         } else {
-            try renderExpression(gpa, ais, tree, while_node.ast.then_expr, .space);
+            try renderExpression(gpa, ais, tree, then_expr, .space);
         }
 
-        var last_else_token = while_node.else_token;
+        var last_else_token = else_token;
 
-        if (while_node.error_token) |error_token| {
-            try renderToken(ais, tree, while_node.else_token, .space); // else
+        if (maybe_error_token) |error_token| {
+            try renderToken(ais, tree, else_token, .space); // else
             try renderToken(ais, tree, error_token - 1, .none); // |
             try renderIdentifier(ais, tree, error_token, .none, .preserve_when_shadowing); // identifier
             last_else_token = error_token + 1; // |
         }
 
         const indent_else_expr = indent_then_expr and
-            !nodeIsBlock(node_tags[while_node.ast.else_expr]) and
-            !nodeIsIfForWhileSwitch(node_tags[while_node.ast.else_expr]);
+            !nodeIsBlock(node_tags[else_expr]) and
+            !nodeIsIfForWhileSwitch(node_tags[else_expr]);
         if (indent_else_expr) {
             ais.pushIndentNextLine();
             try renderToken(ais, tree, last_else_token, .newline);
             ais.popIndent();
-            try renderExpressionIndented(gpa, ais, tree, while_node.ast.else_expr, space);
+            try renderExpressionIndented(gpa, ais, tree, else_expr, space);
         } else {
             try renderToken(ais, tree, last_else_token, .space);
-            try renderExpression(gpa, ais, tree, while_node.ast.else_expr, space);
+            try renderExpression(gpa, ais, tree, else_expr, space);
         }
     } else {
         if (indent_then_expr) {
-            try renderExpressionIndented(gpa, ais, tree, while_node.ast.then_expr, space);
+            try renderExpressionIndented(gpa, ais, tree, then_expr, space);
         } else {
-            try renderExpression(gpa, ais, tree, while_node.ast.then_expr, space);
+            try renderExpression(gpa, ais, tree, then_expr, space);
+        }
+    }
+}
+
+fn renderFor(gpa: Allocator, ais: *Ais, tree: Ast, for_node: Ast.full.For, space: Space) Error!void {
+    const token_tags = tree.tokens.items(.tag);
+
+    if (for_node.label_token) |label| {
+        try renderIdentifier(ais, tree, label, .none, .eagerly_unquote); // label
+        try renderToken(ais, tree, label + 1, .space); // :
+    }
+
+    if (for_node.inline_token) |inline_token| {
+        try renderToken(ais, tree, inline_token, .space); // inline
+    }
+
+    try renderToken(ais, tree, for_node.ast.for_token, .space); // if/for/while
+
+    const lparen = for_node.ast.for_token + 1;
+    try renderParamList(gpa, ais, tree, lparen, for_node.ast.inputs, .space);
+
+    var cur = for_node.payload_token;
+    const pipe = std.mem.indexOfScalarPos(std.zig.Token.Tag, token_tags, cur, .pipe).?;
+    if (token_tags[pipe - 1] == .comma) {
+        ais.pushIndentNextLine();
+        try renderToken(ais, tree, cur - 1, .newline); // |
+        while (true) {
+            if (token_tags[cur] == .asterisk) {
+                try renderToken(ais, tree, cur, .none); // *
+                cur += 1;
+            }
+            try renderIdentifier(ais, tree, cur, .none, .preserve_when_shadowing); // identifier
+            cur += 1;
+            if (token_tags[cur] == .comma) {
+                try renderToken(ais, tree, cur, .newline); // ,
+                cur += 1;
+            }
+            if (token_tags[cur] == .pipe) {
+                break;
+            }
+        }
+        ais.popIndent();
+    } else {
+        try renderToken(ais, tree, cur - 1, .none); // |
+        while (true) {
+            if (token_tags[cur] == .asterisk) {
+                try renderToken(ais, tree, cur, .none); // *
+                cur += 1;
+            }
+            try renderIdentifier(ais, tree, cur, .none, .preserve_when_shadowing); // identifier
+            cur += 1;
+            if (token_tags[cur] == .comma) {
+                try renderToken(ais, tree, cur, .space); // ,
+                cur += 1;
+            }
+            if (token_tags[cur] == .pipe) {
+                break;
+            }
         }
     }
+
+    try renderThenElse(
+        gpa,
+        ais,
+        tree,
+        cur,
+        for_node.ast.then_expr,
+        for_node.else_token,
+        null,
+        for_node.ast.else_expr,
+        space,
+    );
 }
 
 fn renderContainerField(
@@ -2206,15 +2312,23 @@ fn renderCall(
     call: Ast.full.Call,
     space: Space,
 ) Error!void {
-    const token_tags = tree.tokens.items(.tag);
-
     if (call.async_token) |async_token| {
         try renderToken(ais, tree, async_token, .space);
     }
     try renderExpression(gpa, ais, tree, call.ast.fn_expr, .none);
+    try renderParamList(gpa, ais, tree, call.ast.lparen, call.ast.params, space);
+}
+
+fn renderParamList(
+    gpa: Allocator,
+    ais: *Ais,
+    tree: Ast,
+    lparen: Ast.TokenIndex,
+    params: []const Ast.Node.Index,
+    space: Space,
+) Error!void {
+    const token_tags = tree.tokens.items(.tag);
 
-    const lparen = call.ast.lparen;
-    const params = call.ast.params;
     if (params.len == 0) {
         ais.pushIndentNextLine();
         try renderToken(ais, tree, lparen, .none);