Commit d869133a9f

Isaac Freund <ifreund@ifreund.xyz>
2021-02-08 13:38:24
zig fmt: implement switches
1 parent 837cd74
Changed files (4)
lib/std/zig/ast.zig
@@ -213,6 +213,7 @@ pub const Tree = struct {
             .Await,
             .OptionalType,
             .Switch,
+            .SwitchComma,
             .IfSimple,
             .If,
             .Suspend,
@@ -313,7 +314,6 @@ pub const Tree = struct {
             .StructInit,
             .CallOne,
             .Call,
-            .SwitchCaseOne,
             .SwitchRange,
             .FnDecl,
             .ErrorUnion,
@@ -406,7 +406,19 @@ pub const Tree = struct {
                 };
             },
 
-            .SwitchCaseMulti => unreachable, // TODO
+            .SwitchCaseOne => {
+                if (datas[n].lhs == 0) {
+                    return main_tokens[n] - 1; // else token
+                } else {
+                    n = datas[n].lhs;
+                }
+            },
+            .SwitchCase => {
+                const extra = tree.extraData(datas[n].lhs, Node.SubRange);
+                assert(extra.end - extra.start > 0);
+                n = extra.start;
+            },
+
             .WhileSimple => unreachable, // TODO
             .WhileCont => unreachable, // TODO
             .While => unreachable, // TODO
@@ -494,6 +506,8 @@ pub const Tree = struct {
             .PtrTypeSentinel,
             .PtrType,
             .PtrTypeBitRange,
+            .SwitchCaseOne,
+            .SwitchCase,
             => n = datas[n].rhs,
 
             .FieldAccess,
@@ -532,6 +546,16 @@ pub const Tree = struct {
                 }
                 n = tree.extra_data[params.end - 1]; // last parameter
             },
+            .Switch => {
+                const cases = tree.extraData(datas[n].rhs, Node.SubRange);
+                if (cases.end - cases.start == 0) {
+                    end_offset += 3; // rparen, lbrace, rbrace
+                    n = datas[n].lhs; // condition expression
+                } else {
+                    end_offset += 1; // for the rbrace
+                    n = tree.extra_data[cases.end - 1]; // last case
+                }
+            },
             .ContainerDeclArg => {
                 const members = tree.extraData(datas[n].rhs, Node.SubRange);
                 if (members.end - members.start == 0) {
@@ -542,7 +566,9 @@ pub const Tree = struct {
                     n = tree.extra_data[members.end - 1]; // last parameter
                 }
             },
-            .ContainerDeclArgComma => {
+            .ContainerDeclArgComma,
+            .SwitchComma,
+            => {
                 const members = tree.extraData(datas[n].rhs, Node.SubRange);
                 assert(members.end - members.start > 0);
                 end_offset += 2; // for the comma + rbrace
@@ -737,16 +763,13 @@ pub const Tree = struct {
 
             .TaggedUnionEnumTag => unreachable, // TODO
             .TaggedUnionEnumTagComma => unreachable, // TODO
-            .Switch => unreachable, // TODO
             .If => unreachable, // TODO
             .Continue => unreachable, // TODO
             .AsmSimple => unreachable, // TODO
             .Asm => unreachable, // TODO
-            .SwitchCaseOne => unreachable, // TODO
             .SwitchRange => unreachable, // TODO
             .ArrayType => unreachable, // TODO
             .ArrayTypeSentinel => unreachable, // TODO
-            .SwitchCaseMulti => unreachable, // TODO
             .WhileCont => unreachable, // TODO
             .While => unreachable, // TODO
             .ForSimple => unreachable, // TODO
@@ -1202,7 +1225,8 @@ pub const Tree = struct {
     }
 
     pub fn containerDeclArg(tree: Tree, node: Node.Index) Full.ContainerDecl {
-        assert(tree.nodes.items(.tag)[node] == .ContainerDeclArg);
+        assert(tree.nodes.items(.tag)[node] == .ContainerDeclArg or
+            tree.nodes.items(.tag)[node] == .ContainerDeclArgComma);
         const data = tree.nodes.items(.data)[node];
         const members_range = tree.extraData(data.rhs, Node.SubRange);
         return tree.fullContainerDecl(.{
@@ -1214,7 +1238,8 @@ pub const Tree = struct {
     }
 
     pub fn taggedUnionTwo(tree: Tree, buffer: *[2]Node.Index, node: Node.Index) Full.ContainerDecl {
-        assert(tree.nodes.items(.tag)[node] == .TaggedUnionTwo);
+        assert(tree.nodes.items(.tag)[node] == .TaggedUnionTwo or
+            tree.nodes.items(.tag)[node] == .TaggedUnionTwoComma);
         const data = tree.nodes.items(.data)[node];
         buffer.* = .{ data.lhs, data.rhs };
         const members = if (data.rhs != 0)
@@ -1233,7 +1258,8 @@ pub const Tree = struct {
     }
 
     pub fn taggedUnion(tree: Tree, node: Node.Index) Full.ContainerDecl {
-        assert(tree.nodes.items(.tag)[node] == .TaggedUnion);
+        assert(tree.nodes.items(.tag)[node] == .TaggedUnion or
+            tree.nodes.items(.tag)[node] == .TaggedUnionComma);
         const data = tree.nodes.items(.data)[node];
         const main_token = tree.nodes.items(.main_token)[node];
         return tree.fullContainerDecl(.{
@@ -1245,7 +1271,8 @@ pub const Tree = struct {
     }
 
     pub fn taggedUnionEnumTag(tree: Tree, node: Node.Index) Full.ContainerDecl {
-        assert(tree.nodes.items(.tag)[node] == .TaggedUnionEnumTag);
+        assert(tree.nodes.items(.tag)[node] == .TaggedUnionEnumTag or
+            tree.nodes.items(.tag)[node] == .TaggedUnionEnumTagComma);
         const data = tree.nodes.items(.data)[node];
         const members_range = tree.extraData(data.rhs, Node.SubRange);
         const main_token = tree.nodes.items(.main_token)[node];
@@ -1257,6 +1284,25 @@ pub const Tree = struct {
         });
     }
 
+    pub fn switchCaseOne(tree: Tree, node: Node.Index) Full.SwitchCase {
+        const data = &tree.nodes.items(.data)[node];
+        return tree.fullSwitchCase(.{
+            .values = if (data.lhs == 0) &.{} else @ptrCast([*]Node.Index, &data.lhs)[0..1],
+            .arrow_token = tree.nodes.items(.main_token)[node],
+            .target_expr = data.rhs,
+        });
+    }
+
+    pub fn switchCase(tree: Tree, node: Node.Index) Full.SwitchCase {
+        const data = tree.nodes.items(.data)[node];
+        const extra = tree.extraData(data.lhs, Node.SubRange);
+        return tree.fullSwitchCase(.{
+            .values = tree.extra_data[extra.start..extra.end],
+            .arrow_token = tree.nodes.items(.main_token)[node],
+            .target_expr = data.rhs,
+        });
+    }
+
     fn fullVarDecl(tree: Tree, info: Full.VarDecl.Ast) Full.VarDecl {
         const token_tags = tree.tokens.items(.tag);
         var result: Full.VarDecl = .{
@@ -1407,6 +1453,18 @@ pub const Tree = struct {
         }
         return result;
     }
+
+    fn fullSwitchCase(tree: Tree, info: Full.SwitchCase.Ast) Full.SwitchCase {
+        const token_tags = tree.tokens.items(.tag);
+        var result: Full.SwitchCase = .{
+            .ast = info,
+            .payload_token = null,
+        };
+        if (token_tags[info.arrow_token + 1] == .Pipe) {
+            result.payload_token = info.arrow_token + 2;
+        }
+        return result;
+    }
 };
 
 /// Fully assembled AST node information.
@@ -1552,6 +1610,20 @@ pub const Full = struct {
             arg: Node.Index,
         };
     };
+
+    pub const SwitchCase = struct {
+        /// Points to the first token after the `|`. Will either be an identifier or
+        /// a `*` (with an identifier immediately after it).
+        payload_token: ?TokenIndex,
+        ast: Ast,
+
+        pub const Ast = struct {
+            /// If empty, this is an else case
+            values: []const Node.Index,
+            arrow_token: TokenIndex,
+            target_expr: Node.Index,
+        };
+    };
 };
 
 pub const Error = union(enum) {
@@ -1996,13 +2068,16 @@ pub const Node = struct {
         /// `lhs(a, b, c)`. `sub_range_list[rhs]`.
         /// main_token is the `(`.
         Call,
-        /// `switch(lhs) {}`. `sub_range_list[rhs]`.
+        /// `switch(lhs) {}`. `SubRange[rhs]`.
         Switch,
+        /// Same as Switch except there is known to be a trailing comma
+        /// before the final rbrace
+        SwitchComma,
         /// `lhs => rhs`. If lhs is omitted it means `else`.
         /// main_token is the `=>`
         SwitchCaseOne,
-        /// `a, b, c => rhs`. `sub_range_list[lhs]`.
-        SwitchCaseMulti,
+        /// `a, b, c => rhs`. `SubRange[lhs]`.
+        SwitchCase,
         /// `lhs...rhs`.
         SwitchRange,
         /// `while (lhs) rhs`.
lib/std/zig/parse.zig
@@ -2887,10 +2887,11 @@ const Parser = struct {
         _ = try p.expectToken(.RParen);
         _ = try p.expectToken(.LBrace);
         const cases = try p.parseSwitchProngList();
+        const trailing_comma = p.token_tags[p.tok_i - 1] == .Comma;
         _ = try p.expectToken(.RBrace);
 
         return p.addNode(.{
-            .tag = .Switch,
+            .tag = if (trailing_comma) .SwitchComma else .Switch,
             .main_token = switch_token,
             .data = .{
                 .lhs = expr_node,
@@ -3208,7 +3209,7 @@ const Parser = struct {
         const arrow_token = try p.expectToken(.EqualAngleBracketRight);
         _ = try p.parsePtrPayload();
         return p.addNode(.{
-            .tag = .SwitchCaseMulti,
+            .tag = .SwitchCase,
             .main_token = arrow_token,
             .data = .{
                 .lhs = try p.addExtra(Node.SubRange{
lib/std/zig/parser_test.zig
@@ -1671,32 +1671,32 @@ test "zig fmt: block in slice expression" {
 //        \\
 //    );
 //}
-//
-//test "zig fmt: switch cases trailing comma" {
-//    try testTransform(
-//        \\fn switch_cases(x: i32) void {
-//        \\    switch (x) {
-//        \\        1,2,3 => {},
-//        \\        4,5, => {},
-//        \\        6... 8, => {},
-//        \\        else => {},
-//        \\    }
-//        \\}
-//    ,
-//        \\fn switch_cases(x: i32) void {
-//        \\    switch (x) {
-//        \\        1, 2, 3 => {},
-//        \\        4,
-//        \\        5,
-//        \\        => {},
-//        \\        6...8 => {},
-//        \\        else => {},
-//        \\    }
-//        \\}
-//        \\
-//    );
-//}
-//
+
+test "zig fmt: switch cases trailing comma" {
+    try testTransform(
+        \\test "switch cases trailing comma"{
+        \\    switch (x) {
+        \\        1,2,3 => {},
+        \\        4,5, => {},
+        \\        6... 8, => {},
+        \\        else => {},
+        \\    }
+        \\}
+    ,
+        \\test "switch cases trailing comma" {
+        \\    switch (x) {
+        \\        1, 2, 3 => {},
+        \\        4,
+        \\        5,
+        \\        => {},
+        \\        6...8 => {},
+        \\        else => {},
+        \\    }
+        \\}
+        \\
+    );
+}
+
 //test "zig fmt: slice align" {
 //    try testCanonical(
 //        \\const A = struct {
@@ -1996,16 +1996,16 @@ test "zig fmt: ptr deref operator and unwrap optional operator" {
 //        \\
 //    );
 //}
-//
-//test "zig fmt: switch with empty body" {
-//    try testCanonical(
-//        \\test "" {
-//        \\    foo() catch |err| switch (err) {};
-//        \\}
-//        \\
-//    );
-//}
-//
+
+test "zig fmt: switch with empty body" {
+    try testCanonical(
+        \\test "" {
+        \\    foo() catch |err| switch (err) {};
+        \\}
+        \\
+    );
+}
+
 //test "zig fmt: line comments in struct initializer" {
 //    try testCanonical(
 //        \\fn foo() void {
@@ -2725,42 +2725,42 @@ test "zig fmt: blocks" {
     );
 }
 
-//test "zig fmt: switch" {
-//    try testCanonical(
-//        \\test "switch" {
-//        \\    switch (0) {
-//        \\        0 => {},
-//        \\        1 => unreachable,
-//        \\        2, 3 => {},
-//        \\        4...7 => {},
-//        \\        1 + 4 * 3 + 22 => {},
-//        \\        else => {
-//        \\            const a = 1;
-//        \\            const b = a;
-//        \\        },
-//        \\    }
-//        \\
-//        \\    const res = switch (0) {
-//        \\        0 => 0,
-//        \\        1 => 2,
-//        \\        1 => a = 4,
-//        \\        else => 4,
-//        \\    };
-//        \\
-//        \\    const Union = union(enum) {
-//        \\        Int: i64,
-//        \\        Float: f64,
-//        \\    };
-//        \\
-//        \\    switch (u) {
-//        \\        Union.Int => |int| {},
-//        \\        Union.Float => |*float| unreachable,
-//        \\    }
-//        \\}
-//        \\
-//    );
-//}
-//
+test "zig fmt: switch" {
+    try testCanonical(
+        \\test "switch" {
+        \\    switch (0) {
+        \\        0 => {},
+        \\        1 => unreachable,
+        \\        2, 3 => {},
+        \\        4...7 => {},
+        \\        1 + 4 * 3 + 22 => {},
+        \\        else => {
+        \\            const a = 1;
+        \\            const b = a;
+        \\        },
+        \\    }
+        \\
+        \\    const res = switch (0) {
+        \\        0 => 0,
+        \\        1 => 2,
+        \\        1 => a = 4,
+        \\        else => 4,
+        \\    };
+        \\
+        \\    const Union = union(enum) {
+        \\        Int: i64,
+        \\        Float: f64,
+        \\    };
+        \\
+        \\    switch (u) {
+        \\        Union.Int => |int| {},
+        \\        Union.Float => |*float| unreachable,
+        \\    }
+        \\}
+        \\
+    );
+}
+
 //test "zig fmt: while" {
 //    try testCanonical(
 //        \\test "while" {
lib/std/zig/render.zig
@@ -207,19 +207,18 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
         => {
             const statements = [2]ast.Node.Index{ datas[node].lhs, datas[node].rhs };
             if (datas[node].lhs == 0) {
-                return renderBlock(ais, tree, main_tokens[node], statements[0..0], space);
+                return renderBlock(ais, tree, node, statements[0..0], space);
             } else if (datas[node].rhs == 0) {
-                return renderBlock(ais, tree, main_tokens[node], statements[0..1], space);
+                return renderBlock(ais, tree, node, statements[0..1], space);
             } else {
-                return renderBlock(ais, tree, main_tokens[node], statements[0..2], space);
+                return renderBlock(ais, tree, node, statements[0..2], space);
             }
         },
         .Block,
         .BlockSemicolon,
         => {
-            const lbrace = main_tokens[node];
             const statements = tree.extra_data[datas[node].lhs..datas[node].rhs];
-            return renderBlock(ais, tree, main_tokens[node], statements, space);
+            return renderBlock(ais, tree, node, statements, space);
         },
 
         .ErrDefer => {
@@ -615,81 +614,6 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
                 try renderToken(ais, tree, rbrace, space);
             }
         },
-        //.ErrorSetDecl => {
-        //    const err_set_decl = @fieldParentPtr(ast.Node.ErrorSetDecl, "base", base);
-
-        //    const lbrace = tree.nextToken(err_set_decl.error_token);
-
-        //    if (err_set_decl.decls_len == 0) {
-        //        try renderToken(ais, tree, err_set_decl.error_token, Space.None);
-        //        try renderToken(ais, tree, lbrace, Space.None);
-        //        return renderToken(ais, tree, err_set_decl.rbrace_token, space);
-        //    }
-
-        //    if (err_set_decl.decls_len == 1) blk: {
-        //        const node = err_set_decl.decls()[0];
-
-        //        // if there are any doc comments or same line comments
-        //        // don't try to put it all on one line
-        //        if (node.cast(ast.Node.ErrorTag)) |tag| {
-        //            if (tag.doc_comments != null) break :blk;
-        //        } else {
-        //            break :blk;
-        //        }
-
-        //        try renderToken(ais, tree, err_set_decl.error_token, Space.None); // error
-        //        try renderToken(ais, tree, lbrace, Space.None); // lbrace
-        //        try renderExpression(ais, tree, node, Space.None);
-        //        return renderToken(ais, tree, err_set_decl.rbrace_token, space); // rbrace
-        //    }
-
-        //    try renderToken(ais, tree, err_set_decl.error_token, Space.None); // error
-
-        //    const src_has_trailing_comma = blk: {
-        //        const maybe_comma = tree.prevToken(err_set_decl.rbrace_token);
-        //        break :blk tree.token_tags[maybe_comma] == .Comma;
-        //    };
-
-        //    if (src_has_trailing_comma) {
-        //        {
-        //            ais.pushIndent();
-        //            defer ais.popIndent();
-
-        //            try renderToken(ais, tree, lbrace, Space.Newline); // lbrace
-        //            const decls = err_set_decl.decls();
-        //            for (decls) |node, i| {
-        //                if (i + 1 < decls.len) {
-        //                    try renderExpression(ais, tree, node, Space.None);
-        //                    try renderToken(ais, tree, tree.nextToken(node.lastToken()), Space.Newline); // ,
-
-        //                    try renderExtraNewline(ais, tree, decls[i + 1]);
-        //                } else {
-        //                    try renderExpression(ais, tree, node, Space.Comma);
-        //                }
-        //            }
-        //        }
-
-        //        return renderToken(ais, tree, err_set_decl.rbrace_token, space); // rbrace
-        //    } else {
-        //        try renderToken(ais, tree, lbrace, Space.Space); // lbrace
-
-        //        const decls = err_set_decl.decls();
-        //        for (decls) |node, i| {
-        //            if (i + 1 < decls.len) {
-        //                try renderExpression(ais, tree, node, Space.None);
-
-        //                const comma_token = tree.nextToken(node.lastToken());
-        //                assert(tree.token_tags[comma_token] == .Comma);
-        //                try renderToken(ais, tree, comma_token, Space.Space); // ,
-        //                try renderExtraNewline(ais, tree, decls[i + 1]);
-        //            } else {
-        //                try renderExpression(ais, tree, node, Space.Space);
-        //            }
-        //        }
-
-        //        return renderToken(ais, tree, err_set_decl.rbrace_token, space); // rbrace
-        //    }
-        //},
 
         .BuiltinCallTwo, .BuiltinCallTwoComma => {
             if (datas[node].lhs == 0) {
@@ -732,92 +656,38 @@ fn renderExpression(ais: *Ais, tree: ast.Tree, node: ast.Node.Index, space: Spac
         //    }
         //},
 
-        .Switch => unreachable, // TODO
-        //.Switch => {
-        //    const switch_node = @fieldParentPtr(ast.Node.Switch, "base", base);
-
-        //    try renderToken(ais, tree, switch_node.switch_token, Space.Space); // switch
-        //    try renderToken(ais, tree, tree.nextToken(switch_node.switch_token), Space.None); // (
-
-        //    const rparen = tree.nextToken(switch_node.expr.lastToken());
-        //    const lbrace = tree.nextToken(rparen);
-
-        //    if (switch_node.cases_len == 0) {
-        //        try renderExpression(ais, tree, switch_node.expr, Space.None);
-        //        try renderToken(ais, tree, rparen, Space.Space); // )
-        //        try renderToken(ais, tree, lbrace, Space.None); // lbrace
-        //        return renderToken(ais, tree, switch_node.rbrace, space); // rbrace
-        //    }
-
-        //    try renderExpression(ais, tree, switch_node.expr, Space.None);
-        //    try renderToken(ais, tree, rparen, Space.Space); // )
-
-        //    {
-        //        ais.pushIndentNextLine();
-        //        defer ais.popIndent();
-        //        try renderToken(ais, tree, lbrace, Space.Newline); // lbrace
-
-        //        const cases = switch_node.cases();
-        //        for (cases) |node, i| {
-        //            try renderExpression(ais, tree, node, Space.Comma);
-
-        //            if (i + 1 < cases.len) {
-        //                try renderExtraNewline(ais, tree, cases[i + 1]);
-        //            }
-        //        }
-        //    }
-
-        //    return renderToken(ais, tree, switch_node.rbrace, space); // rbrace
-        //},
-
-        .SwitchCaseOne => unreachable, // TODO
-        .SwitchCaseMulti => unreachable, // TODO
-        //.SwitchCase => {
-        //    const switch_case = @fieldParentPtr(ast.Node.SwitchCase, "base", base);
-
-        //    assert(switch_case.items_len != 0);
-        //    const src_has_trailing_comma = blk: {
-        //        const last_node = switch_case.items()[switch_case.items_len - 1];
-        //        const maybe_comma = tree.nextToken(last_node.lastToken());
-        //        break :blk tree.token_tags[maybe_comma] == .Comma;
-        //    };
-
-        //    if (switch_case.items_len == 1 or !src_has_trailing_comma) {
-        //        const items = switch_case.items();
-        //        for (items) |node, i| {
-        //            if (i + 1 < items.len) {
-        //                try renderExpression(ais, tree, node, Space.None);
-
-        //                const comma_token = tree.nextToken(node.lastToken());
-        //                try renderToken(ais, tree, comma_token, Space.Space); // ,
-        //                try renderExtraNewline(ais, tree, items[i + 1]);
-        //            } else {
-        //                try renderExpression(ais, tree, node, Space.Space);
-        //            }
-        //        }
-        //    } else {
-        //        const items = switch_case.items();
-        //        for (items) |node, i| {
-        //            if (i + 1 < items.len) {
-        //                try renderExpression(ais, tree, node, Space.None);
-
-        //                const comma_token = tree.nextToken(node.lastToken());
-        //                try renderToken(ais, tree, comma_token, Space.Newline); // ,
-        //                try renderExtraNewline(ais, tree, items[i + 1]);
-        //            } else {
-        //                try renderExpression(ais, tree, node, Space.Comma);
-        //            }
-        //        }
-        //    }
-
-        //    try renderToken(ais, tree, switch_case.arrow_token, Space.Space); // =>
-
-        //    if (switch_case.payload) |payload| {
-        //        try renderExpression(ais, tree, payload, Space.Space);
-        //    }
+        .Switch,
+        .SwitchComma,
+        => {
+            const switch_token = main_tokens[node];
+            const condition = datas[node].lhs;
+            const extra = tree.extraData(datas[node].rhs, ast.Node.SubRange);
+            const cases = tree.extra_data[extra.start..extra.end];
+            const rparen = tree.lastToken(condition) + 1;
+
+            try renderToken(ais, tree, switch_token, .Space); // switch keyword
+            try renderToken(ais, tree, switch_token + 1, .None); // lparen
+            try renderExpression(ais, tree, condition, .None); // condtion expression
+            try renderToken(ais, tree, rparen, .Space); // rparen
+
+            if (cases.len == 0) {
+                try renderToken(ais, tree, rparen + 1, .None); // lbrace
+                try renderToken(ais, tree, rparen + 2, space); // rbrace
+            } else {
+                try renderToken(ais, tree, rparen + 1, .Newline); // lbrace
+                ais.pushIndent();
+                try renderExpression(ais, tree, cases[0], .Comma);
+                for (cases[1..]) |case| {
+                    try renderExtraNewline(ais, tree, case);
+                    try renderExpression(ais, tree, case, .Comma);
+                }
+                ais.popIndent();
+                try renderToken(ais, tree, tree.lastToken(node), space); // rbrace
+            }
+        },
 
-        //    return renderExpression(ais, tree, switch_case.expr, space);
-        //},
+        .SwitchCaseOne => try renderSwitchCase(ais, tree, tree.switchCaseOne(node), space),
+        .SwitchCase => try renderSwitchCase(ais, tree, tree.switchCase(node), space),
 
         .WhileSimple => unreachable, // TODO
         .WhileCont => unreachable, // TODO
@@ -1745,16 +1615,64 @@ fn renderFnProto(ais: *Ais, tree: ast.Tree, fn_proto: ast.Full.FnProto, space: S
     return renderExpression(ais, tree, fn_proto.ast.return_type, space);
 }
 
+fn renderSwitchCase(
+    ais: *Ais,
+    tree: ast.Tree,
+    switch_case: ast.Full.SwitchCase,
+    space: Space,
+) Error!void {
+    const token_tags = tree.tokens.items(.tag);
+    const trailing_comma = token_tags[switch_case.ast.arrow_token - 1] == .Comma;
+
+    // Render everything before the arrow
+    if (switch_case.ast.values.len == 0) {
+        try renderToken(ais, tree, switch_case.ast.arrow_token - 1, .Space); // else keyword
+    } else if (switch_case.ast.values.len == 1) {
+        // render on one line and drop the trailing comma if any
+        try renderExpression(ais, tree, switch_case.ast.values[0], .Space);
+    } else if (trailing_comma) {
+        // Render each value on a new line
+        try renderExpression(ais, tree, switch_case.ast.values[0], .Comma);
+        for (switch_case.ast.values[1..]) |value_expr| {
+            try renderExtraNewline(ais, tree, value_expr);
+            try renderExpression(ais, tree, value_expr, .Comma);
+        }
+    } else {
+        // Render on one line
+        for (switch_case.ast.values) |value_expr| {
+            try renderExpression(ais, tree, value_expr, .CommaSpace);
+        }
+    }
+
+    // Render the arrow and everything after it
+    try renderToken(ais, tree, switch_case.ast.arrow_token, .Space);
+
+    if (switch_case.payload_token) |payload_token| {
+        try renderToken(ais, tree, payload_token - 1, .None); // pipe
+        if (token_tags[payload_token] == .Asterisk) {
+            try renderToken(ais, tree, payload_token, .None); // asterisk
+            try renderToken(ais, tree, payload_token + 1, .None); // identifier
+            try renderToken(ais, tree, payload_token + 2, .Space); // pipe
+        } else {
+            try renderToken(ais, tree, payload_token, .None); // identifier
+            try renderToken(ais, tree, payload_token + 1, .Space); // pipe
+        }
+    }
+
+    try renderExpression(ais, tree, switch_case.ast.target_expr, space);
+}
+
 fn renderBlock(
     ais: *Ais,
     tree: ast.Tree,
-    lbrace: ast.TokenIndex,
+    block_node: ast.Node.Index,
     statements: []const ast.Node.Index,
     space: Space,
 ) Error!void {
     const token_tags = tree.tokens.items(.tag);
     const node_tags = tree.nodes.items(.tag);
     const nodes_data = tree.nodes.items(.data);
+    const lbrace = tree.nodes.items(.main_token)[block_node];
 
     if (token_tags[lbrace - 1] == .Colon and
         token_tags[lbrace - 2] == .Identifier)
@@ -1783,15 +1701,8 @@ fn renderBlock(
         }
     }
     ais.popIndent();
-    // The rbrace could be +1 or +2 from the last token of the last
-    // statement in the block because lastToken() does not count semicolons.
-    const maybe_rbrace = tree.lastToken(statements[statements.len - 1]) + 1;
-    if (token_tags[maybe_rbrace] == .RBrace) {
-        return renderToken(ais, tree, maybe_rbrace, space);
-    } else {
-        assert(token_tags[maybe_rbrace + 1] == .RBrace);
-        return renderToken(ais, tree, maybe_rbrace + 1, space);
-    }
+
+    try renderToken(ais, tree, tree.lastToken(block_node), space); // rbrace
 }
 
 // TODO: handle comments between fields