Commit c11b6adf13

Jacob Young <jacobly0@users.noreply.github.com>
2024-03-17 02:13:19
Ast: fix comptime destructure
A preceding `comptime` keyword was being ignored if the first destructure variable was an expression.
1 parent d981549
Changed files (8)
lib/compiler/reduce/Walk.zig
@@ -345,12 +345,8 @@ fn walkExpression(w: *Walk, node: Ast.Node.Index) Error!void {
         },
 
         .assign_destructure => {
-            const lhs_count = ast.extra_data[datas[node].lhs];
-            assert(lhs_count > 1);
-            const lhs_exprs = ast.extra_data[datas[node].lhs + 1 ..][0..lhs_count];
-            const rhs = datas[node].rhs;
-
-            for (lhs_exprs) |lhs_node| {
+            const full = tree.assignDestructure(node);
+            for (full.ast.variables) |variable_node| {
                 switch (node_tags[lhs_node]) {
                     .global_var_decl,
                     .local_var_decl,
@@ -358,10 +354,10 @@ fn walkExpression(w: *Walk, node: Ast.Node.Index) Error!void {
                     .aligned_var_decl,
                     => try walkLocalVarDecl(w, ast.fullVarDecl(lhs_node).?),
 
-                    else => try walkExpression(w, lhs_node),
+                    else => try walkExpression(w, variable_node),
                 }
             }
-            return walkExpression(w, rhs);
+            return walkExpression(w, full.ast.assign_expr);
         },
 
         .bit_not,
lib/docs/wasm/Walk.zig
@@ -699,12 +699,9 @@ fn expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, node: Ast.Node.Index)
         },
 
         .assign_destructure => {
-            const extra_index = node_datas[node].lhs;
-            const lhs_count = ast.extra_data[extra_index];
-            const lhs_nodes: []const Ast.Node.Index = @ptrCast(ast.extra_data[extra_index + 1 ..][0..lhs_count]);
-            const rhs = node_datas[node].rhs;
-            for (lhs_nodes) |lhs_node| try expr(w, scope, parent_decl, lhs_node);
-            _ = try expr(w, scope, parent_decl, rhs);
+            const full = ast.assignDestructure(node);
+            for (full.ast.variables) |variable_node| try expr(w, scope, parent_decl, variable_node);
+            _ = try expr(w, scope, parent_decl, full.ast.value_expr);
         },
 
         .bool_not,
lib/std/zig/Ast.zig
@@ -1406,6 +1406,16 @@ pub fn alignedVarDecl(tree: Ast, node: Node.Index) full.VarDecl {
     });
 }
 
+pub fn assignDestructure(tree: Ast, node: Node.Index) full.AssignDestructure {
+    const data = tree.nodes.items(.data)[node];
+    const variable_count = tree.extra_data[data.lhs];
+    return tree.fullAssignDestructureComponents(.{
+        .variables = tree.extra_data[data.lhs + 1 ..][0..variable_count],
+        .equal_token = tree.nodes.items(.main_token)[node],
+        .value_expr = data.rhs,
+    });
+}
+
 pub fn ifSimple(tree: Ast, node: Node.Index) full.If {
     assert(tree.nodes.items(.tag)[node] == .if_simple);
     const data = tree.nodes.items(.data)[node];
@@ -2045,6 +2055,28 @@ fn fullVarDeclComponents(tree: Ast, info: full.VarDecl.Components) full.VarDecl
     return result;
 }
 
+fn fullAssignDestructureComponents(tree: Ast, info: full.AssignDestructure.Components) full.AssignDestructure {
+    const token_tags = tree.tokens.items(.tag);
+    const node_tags = tree.nodes.items(.tag);
+    var result: full.AssignDestructure = .{
+        .comptime_token = null,
+        .ast = info,
+    };
+    const first_variable_token = tree.firstToken(info.variables[0]);
+    const maybe_comptime_token = switch (node_tags[info.variables[0]]) {
+        .global_var_decl,
+        .local_var_decl,
+        .aligned_var_decl,
+        .simple_var_decl,
+        => first_variable_token,
+        else => first_variable_token - 1,
+    };
+    if (token_tags[maybe_comptime_token] == .keyword_comptime) {
+        result.comptime_token = maybe_comptime_token;
+    }
+    return result;
+}
+
 fn fullIfComponents(tree: Ast, info: full.If.Components) full.If {
     const token_tags = tree.tokens.items(.tag);
     var result: full.If = .{
@@ -2508,6 +2540,17 @@ pub const full = struct {
         }
     };
 
+    pub const AssignDestructure = struct {
+        comptime_token: ?TokenIndex,
+        ast: Components,
+
+        pub const Components = struct {
+            variables: []const Node.Index,
+            equal_token: TokenIndex,
+            value_expr: Node.Index,
+        };
+    };
+
     pub const If = struct {
         /// Points to the first token after the `|`. Will either be an identifier or
         /// a `*` (with an identifier immediately after it).
lib/std/zig/AstGen.zig
@@ -3406,45 +3406,36 @@ fn assignDestructure(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerErro
     try emitDbgNode(gz, node);
     const astgen = gz.astgen;
     const tree = astgen.tree;
-    const token_tags = tree.tokens.items(.tag);
-    const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
     const node_tags = tree.nodes.items(.tag);
 
-    const extra_index = node_datas[node].lhs;
-    const lhs_count = tree.extra_data[extra_index];
-    const lhs_nodes: []const Ast.Node.Index = @ptrCast(tree.extra_data[extra_index + 1 ..][0..lhs_count]);
-    const rhs = node_datas[node].rhs;
-
-    const maybe_comptime_token = tree.firstToken(node) - 1;
-    const declared_comptime = token_tags[maybe_comptime_token] == .keyword_comptime;
-
-    if (declared_comptime and gz.is_comptime) {
+    const full = tree.assignDestructure(node);
+    if (full.comptime_token != null and gz.is_comptime) {
         return astgen.failNode(node, "redundant comptime keyword in already comptime scope", .{});
     }
 
     // If this expression is marked comptime, we must wrap the whole thing in a comptime block.
     var gz_buf: GenZir = undefined;
-    const inner_gz = if (declared_comptime) bs: {
+    const inner_gz = if (full.comptime_token) |_| bs: {
         gz_buf = gz.makeSubBlock(scope);
         gz_buf.is_comptime = true;
         break :bs &gz_buf;
     } else gz;
-    defer if (declared_comptime) inner_gz.unstack();
+    defer if (full.comptime_token) |_| inner_gz.unstack();
 
-    const rl_components = try astgen.arena.alloc(ResultInfo.Loc.DestructureComponent, lhs_nodes.len);
-    for (rl_components, lhs_nodes) |*lhs_rl, lhs_node| {
-        if (node_tags[lhs_node] == .identifier) {
+    const rl_components = try astgen.arena.alloc(ResultInfo.Loc.DestructureComponent, full.ast.variables.len);
+    for (rl_components, full.ast.variables) |*variable_rl, variable_node| {
+        if (node_tags[variable_node] == .identifier) {
             // This intentionally does not support `@"_"` syntax.
-            const ident_name = tree.tokenSlice(main_tokens[lhs_node]);
+            const ident_name = tree.tokenSlice(main_tokens[variable_node]);
             if (mem.eql(u8, ident_name, "_")) {
-                lhs_rl.* = .discard;
+                variable_rl.* = .discard;
                 continue;
             }
         }
-        lhs_rl.* = .{ .typed_ptr = .{
-            .inst = try lvalExpr(inner_gz, scope, lhs_node),
-            .src_node = lhs_node,
+        variable_rl.* = .{ .typed_ptr = .{
+            .inst = try lvalExpr(inner_gz, scope, variable_node),
+            .src_node = variable_node,
         } };
     }
 
@@ -3453,9 +3444,9 @@ fn assignDestructure(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerErro
         .components = rl_components,
     } } };
 
-    _ = try expr(inner_gz, scope, ri, rhs);
+    _ = try expr(inner_gz, scope, ri, full.ast.value_expr);
 
-    if (declared_comptime) {
+    if (full.comptime_token) |_| {
         const comptime_block_inst = try gz.makeBlockInst(.block_comptime, node);
         _ = try inner_gz.addBreak(.@"break", comptime_block_inst, .void_value);
         try inner_gz.setBlockBody(comptime_block_inst);
@@ -3474,23 +3465,16 @@ fn assignDestructureMaybeDecls(
     const astgen = gz.astgen;
     const tree = astgen.tree;
     const token_tags = tree.tokens.items(.tag);
-    const node_datas = tree.nodes.items(.data);
     const main_tokens = tree.nodes.items(.main_token);
     const node_tags = tree.nodes.items(.tag);
 
-    const extra_index = node_datas[node].lhs;
-    const lhs_count = tree.extra_data[extra_index];
-    const lhs_nodes: []const Ast.Node.Index = @ptrCast(tree.extra_data[extra_index + 1 ..][0..lhs_count]);
-    const rhs = node_datas[node].rhs;
-
-    const maybe_comptime_token = tree.firstToken(node) - 1;
-    const declared_comptime = token_tags[maybe_comptime_token] == .keyword_comptime;
-    if (declared_comptime and gz.is_comptime) {
+    const full = tree.assignDestructure(node);
+    if (full.comptime_token != null and gz.is_comptime) {
         return astgen.failNode(node, "redundant comptime keyword in already comptime scope", .{});
     }
 
-    const is_comptime = declared_comptime or gz.is_comptime;
-    const rhs_is_comptime = tree.nodes.items(.tag)[rhs] == .@"comptime";
+    const is_comptime = full.comptime_token != null or gz.is_comptime;
+    const value_is_comptime = node_tags[full.ast.value_expr] == .@"comptime";
 
     // When declaring consts via a destructure, we always use a result pointer.
     // This avoids the need to create tuple types, and is also likely easier to
@@ -3499,24 +3483,24 @@ fn assignDestructureMaybeDecls(
 
     // We know this rl information won't live past the evaluation of this
     // expression, so it may as well go in the block arena.
-    const rl_components = try block_arena.alloc(ResultInfo.Loc.DestructureComponent, lhs_nodes.len);
-    var any_non_const_lhs = false;
+    const rl_components = try block_arena.alloc(ResultInfo.Loc.DestructureComponent, full.ast.variables.len);
+    var any_non_const_variables = false;
     var any_lvalue_expr = false;
-    for (rl_components, lhs_nodes) |*lhs_rl, lhs_node| {
-        switch (node_tags[lhs_node]) {
+    for (rl_components, full.ast.variables) |*variable_rl, variable_node| {
+        switch (node_tags[variable_node]) {
             .identifier => {
                 // This intentionally does not support `@"_"` syntax.
-                const ident_name = tree.tokenSlice(main_tokens[lhs_node]);
+                const ident_name = tree.tokenSlice(main_tokens[variable_node]);
                 if (mem.eql(u8, ident_name, "_")) {
-                    any_non_const_lhs = true;
-                    lhs_rl.* = .discard;
+                    any_non_const_variables = true;
+                    variable_rl.* = .discard;
                     continue;
                 }
             },
             .global_var_decl, .local_var_decl, .simple_var_decl, .aligned_var_decl => {
-                const full = tree.fullVarDecl(lhs_node).?;
+                const full_var_decl = tree.fullVarDecl(variable_node).?;
 
-                const name_token = full.ast.mut_token + 1;
+                const name_token = full_var_decl.ast.mut_token + 1;
                 const ident_name_raw = tree.tokenSlice(name_token);
                 if (mem.eql(u8, ident_name_raw, "_")) {
                     return astgen.failTok(name_token, "'_' used as an identifier without @\"_\" syntax", .{});
@@ -3524,35 +3508,35 @@ fn assignDestructureMaybeDecls(
 
                 // We detect shadowing in the second pass over these, while we're creating scopes.
 
-                if (full.ast.addrspace_node != 0) {
-                    return astgen.failTok(main_tokens[full.ast.addrspace_node], "cannot set address space of local variable '{s}'", .{ident_name_raw});
+                if (full_var_decl.ast.addrspace_node != 0) {
+                    return astgen.failTok(main_tokens[full_var_decl.ast.addrspace_node], "cannot set address space of local variable '{s}'", .{ident_name_raw});
                 }
-                if (full.ast.section_node != 0) {
-                    return astgen.failTok(main_tokens[full.ast.section_node], "cannot set section of local variable '{s}'", .{ident_name_raw});
+                if (full_var_decl.ast.section_node != 0) {
+                    return astgen.failTok(main_tokens[full_var_decl.ast.section_node], "cannot set section of local variable '{s}'", .{ident_name_raw});
                 }
 
-                const is_const = switch (token_tags[full.ast.mut_token]) {
+                const is_const = switch (token_tags[full_var_decl.ast.mut_token]) {
                     .keyword_var => false,
                     .keyword_const => true,
                     else => unreachable,
                 };
-                if (!is_const) any_non_const_lhs = true;
+                if (!is_const) any_non_const_variables = true;
 
                 // We also mark `const`s as comptime if the RHS is definitely comptime-known.
-                const this_lhs_comptime = is_comptime or (is_const and rhs_is_comptime);
+                const this_variable_comptime = is_comptime or (is_const and value_is_comptime);
 
-                const align_inst: Zir.Inst.Ref = if (full.ast.align_node != 0)
-                    try expr(gz, scope, coerced_align_ri, full.ast.align_node)
+                const align_inst: Zir.Inst.Ref = if (full_var_decl.ast.align_node != 0)
+                    try expr(gz, scope, coerced_align_ri, full_var_decl.ast.align_node)
                 else
                     .none;
 
-                if (full.ast.type_node != 0) {
+                if (full_var_decl.ast.type_node != 0) {
                     // Typed alloc
-                    const type_inst = try typeExpr(gz, scope, full.ast.type_node);
+                    const type_inst = try typeExpr(gz, scope, full_var_decl.ast.type_node);
                     const ptr = if (align_inst == .none) ptr: {
                         const tag: Zir.Inst.Tag = if (is_const)
                             .alloc
-                        else if (this_lhs_comptime)
+                        else if (this_variable_comptime)
                             .alloc_comptime_mut
                         else
                             .alloc_mut;
@@ -3562,16 +3546,16 @@ fn assignDestructureMaybeDecls(
                         .type_inst = type_inst,
                         .align_inst = align_inst,
                         .is_const = is_const,
-                        .is_comptime = this_lhs_comptime,
+                        .is_comptime = this_variable_comptime,
                     });
-                    lhs_rl.* = .{ .typed_ptr = .{ .inst = ptr } };
+                    variable_rl.* = .{ .typed_ptr = .{ .inst = ptr } };
                 } else {
                     // Inferred alloc
                     const ptr = if (align_inst == .none) ptr: {
                         const tag: Zir.Inst.Tag = if (is_const) tag: {
-                            break :tag if (this_lhs_comptime) .alloc_inferred_comptime else .alloc_inferred;
+                            break :tag if (this_variable_comptime) .alloc_inferred_comptime else .alloc_inferred;
                         } else tag: {
-                            break :tag if (this_lhs_comptime) .alloc_inferred_comptime_mut else .alloc_inferred_mut;
+                            break :tag if (this_variable_comptime) .alloc_inferred_comptime_mut else .alloc_inferred_mut;
                         };
                         break :ptr try gz.addNode(tag, node);
                     } else try gz.addAllocExtended(.{
@@ -3579,48 +3563,48 @@ fn assignDestructureMaybeDecls(
                         .type_inst = .none,
                         .align_inst = align_inst,
                         .is_const = is_const,
-                        .is_comptime = this_lhs_comptime,
+                        .is_comptime = this_variable_comptime,
                     });
-                    lhs_rl.* = .{ .inferred_ptr = ptr };
+                    variable_rl.* = .{ .inferred_ptr = ptr };
                 }
 
                 continue;
             },
             else => {},
         }
-        // This LHS is just an lvalue expression.
+        // This variable is just an lvalue expression.
         // We will fill in its result pointer later, inside a comptime block.
-        any_non_const_lhs = true;
+        any_non_const_variables = true;
         any_lvalue_expr = true;
-        lhs_rl.* = .{ .typed_ptr = .{
+        variable_rl.* = .{ .typed_ptr = .{
             .inst = undefined,
-            .src_node = lhs_node,
+            .src_node = variable_node,
         } };
     }
 
-    if (declared_comptime and !any_non_const_lhs) {
-        try astgen.appendErrorTok(maybe_comptime_token, "'comptime const' is redundant; instead wrap the initialization expression with 'comptime'", .{});
+    if (full.comptime_token != null and !any_non_const_variables) {
+        try astgen.appendErrorTok(full.comptime_token.?, "'comptime const' is redundant; instead wrap the initialization expression with 'comptime'", .{});
     }
 
     // If this expression is marked comptime, we must wrap it in a comptime block.
     var gz_buf: GenZir = undefined;
-    const inner_gz = if (declared_comptime) bs: {
+    const inner_gz = if (full.comptime_token) |_| bs: {
         gz_buf = gz.makeSubBlock(scope);
         gz_buf.is_comptime = true;
         break :bs &gz_buf;
     } else gz;
-    defer if (declared_comptime) inner_gz.unstack();
+    defer if (full.comptime_token) |_| inner_gz.unstack();
 
     if (any_lvalue_expr) {
-        // At least one LHS was an lvalue expr. Iterate again in order to
+        // At least one variable was an lvalue expr. Iterate again in order to
         // evaluate the lvalues from within the possible block_comptime.
-        for (rl_components, lhs_nodes) |*lhs_rl, lhs_node| {
-            if (lhs_rl.* != .typed_ptr) continue;
-            switch (node_tags[lhs_node]) {
+        for (rl_components, full.ast.variables) |*variable_rl, variable_node| {
+            if (variable_rl.* != .typed_ptr) continue;
+            switch (node_tags[variable_node]) {
                 .global_var_decl, .local_var_decl, .simple_var_decl, .aligned_var_decl => continue,
                 else => {},
             }
-            lhs_rl.typed_ptr.inst = try lvalExpr(inner_gz, scope, lhs_node);
+            variable_rl.typed_ptr.inst = try lvalExpr(inner_gz, scope, variable_node);
         }
     }
 
@@ -3629,9 +3613,9 @@ fn assignDestructureMaybeDecls(
     _ = try reachableExpr(inner_gz, scope, .{ .rl = .{ .destructure = .{
         .src_node = node,
         .components = rl_components,
-    } } }, rhs, node);
+    } } }, full.ast.value_expr, node);
 
-    if (declared_comptime) {
+    if (full.comptime_token) |_| {
         // Finish the block_comptime. Inferred alloc resolution etc will occur
         // in the parent block.
         const comptime_block_inst = try gz.makeBlockInst(.block_comptime, node);
@@ -3640,37 +3624,37 @@ fn assignDestructureMaybeDecls(
         try gz.instructions.append(gz.astgen.gpa, comptime_block_inst);
     }
 
-    // Now, iterate over the LHS exprs to construct any new scopes.
+    // Now, iterate over the variable exprs to construct any new scopes.
     // If there were any inferred allocations, resolve them.
     // If there were any `const` decls, make the pointer constant.
     var cur_scope = scope;
-    for (rl_components, lhs_nodes) |lhs_rl, lhs_node| {
-        switch (node_tags[lhs_node]) {
+    for (rl_components, full.ast.variables) |variable_rl, variable_node| {
+        switch (node_tags[variable_node]) {
             .local_var_decl, .simple_var_decl, .aligned_var_decl => {},
             else => continue, // We were mutating an existing lvalue - nothing to do
         }
-        const full = tree.fullVarDecl(lhs_node).?;
-        const raw_ptr = switch (lhs_rl) {
+        const full_var_decl = tree.fullVarDecl(variable_node).?;
+        const raw_ptr = switch (variable_rl) {
             .discard => unreachable,
             .typed_ptr => |typed_ptr| typed_ptr.inst,
             .inferred_ptr => |ptr_inst| ptr_inst,
         };
         // If the alloc was inferred, resolve it.
-        if (full.ast.type_node == 0) {
-            _ = try gz.addUnNode(.resolve_inferred_alloc, raw_ptr, lhs_node);
+        if (full_var_decl.ast.type_node == 0) {
+            _ = try gz.addUnNode(.resolve_inferred_alloc, raw_ptr, variable_node);
         }
-        const is_const = switch (token_tags[full.ast.mut_token]) {
+        const is_const = switch (token_tags[full_var_decl.ast.mut_token]) {
             .keyword_var => false,
             .keyword_const => true,
             else => unreachable,
         };
         // If the alloc was const, make it const.
-        const var_ptr = if (is_const and full.ast.type_node != 0) make_const: {
+        const var_ptr = if (is_const and full_var_decl.ast.type_node != 0) make_const: {
             // Note that we don't do this if type_node == 0 since `resolve_inferred_alloc`
             // handles it for us.
             break :make_const try gz.addUnNode(.make_ptr_const, raw_ptr, node);
         } else raw_ptr;
-        const name_token = full.ast.mut_token + 1;
+        const name_token = full_var_decl.ast.mut_token + 1;
         const ident_name_raw = tree.tokenSlice(name_token);
         const ident_name = try astgen.identAsString(name_token);
         try astgen.detectLocalShadowing(
lib/std/zig/AstRlAnnotate.zig
@@ -204,13 +204,12 @@ fn expr(astrl: *AstRlAnnotate, node: Ast.Node.Index, block: ?*Block, ri: ResultI
             }
         },
         .assign_destructure => {
-            const lhs_count = tree.extra_data[node_datas[node].lhs];
-            const all_lhs = tree.extra_data[node_datas[node].lhs + 1 ..][0..lhs_count];
-            for (all_lhs) |lhs| {
-                _ = try astrl.expr(lhs, block, ResultInfo.none);
+            const full = tree.assignDestructure(node);
+            for (full.ast.variables) |variable_node| {
+                _ = try astrl.expr(variable_node, block, ResultInfo.none);
             }
             // We don't need to gather any meaningful data here, because destructures always use RLS
-            _ = try astrl.expr(node_datas[node].rhs, block, ResultInfo.none);
+            _ = try astrl.expr(full.ast.value_expr, block, ResultInfo.none);
             return false;
         },
         .assign => {
lib/std/zig/parser_test.zig
@@ -2914,6 +2914,25 @@ test "zig fmt: test declaration" {
     );
 }
 
+test "zig fmt: destructure" {
+    try testCanonical(
+        \\comptime {
+        \\    var w: u8, var x: u8 = .{ 1, 2 };
+        \\    w, var y: u8 = .{ 3, 4 };
+        \\    var z: u8, x = .{ 5, 6 };
+        \\    y, z = .{ 7, 8 };
+        \\}
+        \\
+        \\comptime {
+        \\    comptime var w, var x = .{ 1, 2 };
+        \\    comptime w, var y = .{ 3, 4 };
+        \\    comptime var z, x = .{ 5, 6 };
+        \\    comptime y, z = .{ 7, 8 };
+        \\}
+        \\
+    );
+}
+
 test "zig fmt: infix operators" {
     try testCanonical(
         \\test {
lib/std/zig/render.zig
@@ -569,39 +569,33 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
         },
 
         .assign_destructure => {
-            const lhs_count = tree.extra_data[datas[node].lhs];
-            assert(lhs_count > 1);
-            const lhs_exprs = tree.extra_data[datas[node].lhs + 1 ..][0..lhs_count];
-            const rhs = datas[node].rhs;
-
-            const maybe_comptime_token = tree.firstToken(node) - 1;
-            if (token_tags[maybe_comptime_token] == .keyword_comptime) {
-                try renderToken(r, maybe_comptime_token, .space);
+            const full = tree.assignDestructure(node);
+            if (full.comptime_token) |comptime_token| {
+                try renderToken(r, comptime_token, .space);
             }
 
-            for (lhs_exprs, 0..) |lhs_node, i| {
-                const lhs_space: Space = if (i == lhs_exprs.len - 1) .space else .comma_space;
-                switch (node_tags[lhs_node]) {
+            for (full.ast.variables, 0..) |variable_node, i| {
+                const variable_space: Space = if (i == full.ast.variables.len - 1) .space else .comma_space;
+                switch (node_tags[variable_node]) {
                     .global_var_decl,
                     .local_var_decl,
                     .simple_var_decl,
                     .aligned_var_decl,
                     => {
-                        try renderVarDecl(r, tree.fullVarDecl(lhs_node).?, true, lhs_space);
+                        try renderVarDecl(r, tree.fullVarDecl(variable_node).?, true, variable_space);
                     },
-                    else => try renderExpression(r, lhs_node, lhs_space),
+                    else => try renderExpression(r, variable_node, variable_space),
                 }
             }
-            const equal_token = main_tokens[node];
-            if (tree.tokensOnSameLine(equal_token, equal_token + 1)) {
-                try renderToken(r, equal_token, .space);
+            if (tree.tokensOnSameLine(full.ast.equal_token, full.ast.equal_token + 1)) {
+                try renderToken(r, full.ast.equal_token, .space);
             } else {
                 ais.pushIndent();
-                try renderToken(r, equal_token, .newline);
+                try renderToken(r, full.ast.equal_token, .newline);
                 ais.popIndent();
             }
             ais.pushIndentOneShot();
-            return renderExpression(r, rhs, space);
+            return renderExpression(r, full.ast.value_expr, space);
         },
 
         .bit_not,
test/behavior/destructure.zig
@@ -24,21 +24,55 @@ test "simple destructure" {
 
 test "destructure with comptime syntax" {
     const S = struct {
-        fn doTheTest() void {
-            comptime var x: f32 = undefined;
-            comptime x, const y, var z = .{ 0.5, 123, 456 }; // z is a comptime var
-            _ = &z;
-
-            comptime assert(@TypeOf(y) == comptime_int);
-            comptime assert(@TypeOf(z) == comptime_int);
-            comptime assert(x == 0.5);
-            comptime assert(y == 123);
-            comptime assert(z == 456);
+        fn doTheTest() !void {
+            {
+                comptime var x: f32 = undefined;
+                comptime x, const y, var z = .{ 0.5, 123, 456 }; // z is a comptime var
+                _ = &z;
+
+                comptime assert(@TypeOf(y) == comptime_int);
+                comptime assert(@TypeOf(z) == comptime_int);
+                comptime assert(x == 0.5);
+                comptime assert(y == 123);
+                comptime assert(z == 456);
+            }
+            {
+                var w: u8, var x: u8 = .{ 1, 2 };
+                w, var y: u8 = .{ 3, 4 };
+                var z: u8, x = .{ 5, 6 };
+                y, z = .{ 7, 8 };
+                {
+                    w += 1;
+                    x -= 2;
+                    y *= 3;
+                    z /= 4;
+                }
+                try expect(w == 4);
+                try expect(x == 4);
+                try expect(y == 21);
+                try expect(z == 2);
+            }
+            {
+                comptime var w, var x = .{ 1, 2 };
+                comptime w, var y = .{ 3, 4 };
+                comptime var z, x = .{ 5, 6 };
+                comptime y, z = .{ 7, 8 };
+                comptime {
+                    w += 1;
+                    x -= 2;
+                    y *= 3;
+                    z /= 4;
+                }
+                comptime assert(w == 4);
+                comptime assert(x == 4);
+                comptime assert(y == 21);
+                comptime assert(z == 2);
+            }
         }
     };
 
-    S.doTheTest();
-    comptime S.doTheTest();
+    try S.doTheTest();
+    try comptime S.doTheTest();
 }
 
 test "destructure from labeled block" {