master
  1const std = @import("std");
  2const Ast = std.zig.Ast;
  3const Walk = @This();
  4const assert = std.debug.assert;
  5const BuiltinFn = std.zig.BuiltinFn;
  6
  7ast: *const Ast,
  8transformations: *std.array_list.Managed(Transformation),
  9unreferenced_globals: std.StringArrayHashMapUnmanaged(Ast.Node.Index),
 10in_scope_names: std.StringArrayHashMapUnmanaged(u32),
 11replace_names: std.StringArrayHashMapUnmanaged(u32),
 12gpa: std.mem.Allocator,
 13arena: std.mem.Allocator,
 14
 15pub const Transformation = union(enum) {
 16    /// Replace the fn decl AST Node with one whose body is only `@trap()` with
 17    /// discarded parameters.
 18    gut_function: Ast.Node.Index,
 19    /// Omit a global declaration.
 20    delete_node: Ast.Node.Index,
 21    /// Delete a local variable declaration and replace all of its references
 22    /// with `undefined`.
 23    delete_var_decl: struct {
 24        var_decl_node: Ast.Node.Index,
 25        /// Identifier nodes that reference the variable.
 26        references: std.ArrayList(Ast.Node.Index),
 27    },
 28    /// Replace an expression with `undefined`.
 29    replace_with_undef: Ast.Node.Index,
 30    /// Replace an expression with `true`.
 31    replace_with_true: Ast.Node.Index,
 32    /// Replace an expression with `false`.
 33    replace_with_false: Ast.Node.Index,
 34    /// Replace a node with another node.
 35    replace_node: struct {
 36        to_replace: Ast.Node.Index,
 37        replacement: Ast.Node.Index,
 38    },
 39    /// Replace an `@import` with the imported file contents wrapped in a struct.
 40    inline_imported_file: InlineImportedFile,
 41
 42    pub const InlineImportedFile = struct {
 43        builtin_call_node: Ast.Node.Index,
 44        imported_string: []const u8,
 45        /// Identifier names that must be renamed in the inlined code or else
 46        /// will cause ambiguous reference errors.
 47        in_scope_names: std.StringArrayHashMapUnmanaged(void),
 48    };
 49};
 50
 51pub const Error = error{OutOfMemory};
 52
 53/// The result will be priority shuffled.
 54pub fn findTransformations(
 55    arena: std.mem.Allocator,
 56    ast: *const Ast,
 57    transformations: *std.array_list.Managed(Transformation),
 58) !void {
 59    transformations.clearRetainingCapacity();
 60
 61    var walk: Walk = .{
 62        .ast = ast,
 63        .transformations = transformations,
 64        .gpa = transformations.allocator,
 65        .arena = arena,
 66        .unreferenced_globals = .{},
 67        .in_scope_names = .{},
 68        .replace_names = .{},
 69    };
 70    defer {
 71        walk.unreferenced_globals.deinit(walk.gpa);
 72        walk.in_scope_names.deinit(walk.gpa);
 73        walk.replace_names.deinit(walk.gpa);
 74    }
 75
 76    try walkMembers(&walk, walk.ast.rootDecls());
 77
 78    const unreferenced_globals = walk.unreferenced_globals.values();
 79    try transformations.ensureUnusedCapacity(unreferenced_globals.len);
 80    for (unreferenced_globals) |node| {
 81        transformations.appendAssumeCapacity(.{ .delete_node = node });
 82    }
 83}
 84
 85fn walkMembers(w: *Walk, members: []const Ast.Node.Index) Error!void {
 86    // First we scan for globals so that we can delete them while walking.
 87    try scanDecls(w, members, .add);
 88
 89    for (members) |member| {
 90        try walkMember(w, member);
 91    }
 92
 93    try scanDecls(w, members, .remove);
 94}
 95
 96const ScanDeclsAction = enum { add, remove };
 97
 98fn scanDecls(w: *Walk, members: []const Ast.Node.Index, action: ScanDeclsAction) Error!void {
 99    const ast = w.ast;
100    const gpa = w.gpa;
101
102    for (members) |member_node| {
103        const name_token = switch (ast.nodeTag(member_node)) {
104            .global_var_decl,
105            .local_var_decl,
106            .simple_var_decl,
107            .aligned_var_decl,
108            => ast.nodeMainToken(member_node) + 1,
109
110            .fn_proto_simple,
111            .fn_proto_multi,
112            .fn_proto_one,
113            .fn_proto,
114            .fn_decl,
115            => ast.nodeMainToken(member_node) + 1,
116
117            else => continue,
118        };
119
120        assert(ast.tokenTag(name_token) == .identifier);
121        const name_bytes = ast.tokenSlice(name_token);
122
123        switch (action) {
124            .add => {
125                try w.unreferenced_globals.put(gpa, name_bytes, member_node);
126
127                const gop = try w.in_scope_names.getOrPut(gpa, name_bytes);
128                if (!gop.found_existing) gop.value_ptr.* = 0;
129                gop.value_ptr.* += 1;
130            },
131            .remove => {
132                const entry = w.in_scope_names.getEntry(name_bytes).?;
133                if (entry.value_ptr.* <= 1) {
134                    assert(w.in_scope_names.swapRemove(name_bytes));
135                } else {
136                    entry.value_ptr.* -= 1;
137                }
138            },
139        }
140    }
141}
142
143fn walkMember(w: *Walk, decl: Ast.Node.Index) Error!void {
144    const ast = w.ast;
145    switch (ast.nodeTag(decl)) {
146        .fn_decl => {
147            const fn_proto, const body_node = ast.nodeData(decl).node_and_node;
148            try walkExpression(w, fn_proto);
149            if (!isFnBodyGutted(ast, body_node)) {
150                w.replace_names.clearRetainingCapacity();
151                try w.transformations.append(.{ .gut_function = decl });
152                try walkExpression(w, body_node);
153            }
154        },
155        .fn_proto_simple,
156        .fn_proto_multi,
157        .fn_proto_one,
158        .fn_proto,
159        => {
160            try walkExpression(w, decl);
161        },
162
163        .global_var_decl,
164        .local_var_decl,
165        .simple_var_decl,
166        .aligned_var_decl,
167        => try walkGlobalVarDecl(w, decl, ast.fullVarDecl(decl).?),
168
169        .test_decl => {
170            try w.transformations.append(.{ .delete_node = decl });
171            try walkExpression(w, ast.nodeData(decl).opt_token_and_node[1]);
172        },
173
174        .container_field_init,
175        .container_field_align,
176        .container_field,
177        => {
178            try w.transformations.append(.{ .delete_node = decl });
179            try walkContainerField(w, ast.fullContainerField(decl).?);
180        },
181
182        .@"comptime" => {
183            try w.transformations.append(.{ .delete_node = decl });
184            try walkExpression(w, decl);
185        },
186
187        .root => unreachable,
188        else => unreachable,
189    }
190}
191
192fn walkExpression(w: *Walk, node: Ast.Node.Index) Error!void {
193    const ast = w.ast;
194    switch (ast.nodeTag(node)) {
195        .identifier => {
196            const name_ident = ast.nodeMainToken(node);
197            assert(ast.tokenTag(name_ident) == .identifier);
198            const name_bytes = ast.tokenSlice(name_ident);
199            _ = w.unreferenced_globals.swapRemove(name_bytes);
200            if (w.replace_names.get(name_bytes)) |index| {
201                try w.transformations.items[index].delete_var_decl.references.append(w.arena, node);
202            }
203        },
204
205        .number_literal,
206        .char_literal,
207        .unreachable_literal,
208        .anyframe_literal,
209        .string_literal,
210        => {},
211
212        .multiline_string_literal => {},
213
214        .error_value => {},
215
216        .block_two,
217        .block_two_semicolon,
218        .block,
219        .block_semicolon,
220        => {
221            var buf: [2]Ast.Node.Index = undefined;
222            const statements = ast.blockStatements(&buf, node).?;
223            return walkBlock(w, node, statements);
224        },
225
226        .@"errdefer" => {
227            const expr = ast.nodeData(node).opt_token_and_node[1];
228            return walkExpression(w, expr);
229        },
230
231        .@"defer",
232        .@"comptime",
233        .@"nosuspend",
234        .@"suspend",
235        => {
236            return walkExpression(w, ast.nodeData(node).node);
237        },
238
239        .field_access => {
240            try walkExpression(w, ast.nodeData(node).node_and_token[0]);
241        },
242
243        .for_range => {
244            const start, const opt_end = ast.nodeData(node).node_and_opt_node;
245            try walkExpression(w, start);
246            if (opt_end.unwrap()) |end| {
247                return walkExpression(w, end);
248            }
249        },
250
251        .add,
252        .add_wrap,
253        .add_sat,
254        .array_cat,
255        .array_mult,
256        .assign,
257        .assign_bit_and,
258        .assign_bit_or,
259        .assign_shl,
260        .assign_shl_sat,
261        .assign_shr,
262        .assign_bit_xor,
263        .assign_div,
264        .assign_sub,
265        .assign_sub_wrap,
266        .assign_sub_sat,
267        .assign_mod,
268        .assign_add,
269        .assign_add_wrap,
270        .assign_add_sat,
271        .assign_mul,
272        .assign_mul_wrap,
273        .assign_mul_sat,
274        .bang_equal,
275        .bit_and,
276        .bit_or,
277        .shl,
278        .shl_sat,
279        .shr,
280        .bit_xor,
281        .bool_and,
282        .bool_or,
283        .div,
284        .equal_equal,
285        .greater_or_equal,
286        .greater_than,
287        .less_or_equal,
288        .less_than,
289        .merge_error_sets,
290        .mod,
291        .mul,
292        .mul_wrap,
293        .mul_sat,
294        .sub,
295        .sub_wrap,
296        .sub_sat,
297        .@"catch",
298        .error_union,
299        .switch_range,
300        .@"orelse",
301        .array_access,
302        => {
303            const lhs, const rhs = ast.nodeData(node).node_and_node;
304            try walkExpression(w, lhs);
305            try walkExpression(w, rhs);
306        },
307
308        .assign_destructure => {
309            const full = ast.assignDestructure(node);
310            for (full.ast.variables) |variable_node| {
311                switch (ast.nodeTag(variable_node)) {
312                    .global_var_decl,
313                    .local_var_decl,
314                    .simple_var_decl,
315                    .aligned_var_decl,
316                    => try walkLocalVarDecl(w, ast.fullVarDecl(variable_node).?),
317
318                    else => try walkExpression(w, variable_node),
319                }
320            }
321            return walkExpression(w, full.ast.value_expr);
322        },
323
324        .bit_not,
325        .bool_not,
326        .negation,
327        .negation_wrap,
328        .optional_type,
329        .address_of,
330        .@"try",
331        .@"resume",
332        .deref,
333        => {
334            return walkExpression(w, ast.nodeData(node).node);
335        },
336
337        .array_type,
338        .array_type_sentinel,
339        => {},
340
341        .ptr_type_aligned,
342        .ptr_type_sentinel,
343        .ptr_type,
344        .ptr_type_bit_range,
345        => {},
346
347        .array_init_one,
348        .array_init_one_comma,
349        .array_init_dot_two,
350        .array_init_dot_two_comma,
351        .array_init_dot,
352        .array_init_dot_comma,
353        .array_init,
354        .array_init_comma,
355        => {
356            var elements: [2]Ast.Node.Index = undefined;
357            return walkArrayInit(w, ast.fullArrayInit(&elements, node).?);
358        },
359
360        .struct_init_one,
361        .struct_init_one_comma,
362        .struct_init_dot_two,
363        .struct_init_dot_two_comma,
364        .struct_init_dot,
365        .struct_init_dot_comma,
366        .struct_init,
367        .struct_init_comma,
368        => {
369            var buf: [2]Ast.Node.Index = undefined;
370            return walkStructInit(w, node, ast.fullStructInit(&buf, node).?);
371        },
372
373        .call_one,
374        .call_one_comma,
375        .call,
376        .call_comma,
377        => {
378            var buf: [1]Ast.Node.Index = undefined;
379            return walkCall(w, ast.fullCall(&buf, node).?);
380        },
381
382        .slice_open, .slice, .slice_sentinel => return walkSlice(w, node, ast.fullSlice(node).?),
383
384        .unwrap_optional => {
385            try walkExpression(w, ast.nodeData(node).node_and_token[0]);
386        },
387
388        .@"break" => {
389            const label_token, const target = ast.nodeData(node).opt_token_and_opt_node;
390            if (label_token == .none and target == .none) {
391                // no expressions
392            } else if (label_token == .none and target != .none) {
393                try walkExpression(w, target.unwrap().?);
394            } else if (label_token != .none and target == .none) {
395                try walkIdentifier(w, label_token.unwrap().?);
396            } else if (label_token != .none and target != .none) {
397                try walkExpression(w, target.unwrap().?);
398            }
399        },
400
401        .@"continue" => {
402            const opt_label = ast.nodeData(node).opt_token_and_opt_node[0];
403            if (opt_label.unwrap()) |label| {
404                return walkIdentifier(w, label);
405            }
406        },
407
408        .@"return" => {
409            if (ast.nodeData(node).opt_node.unwrap()) |lhs| {
410                try walkExpression(w, lhs);
411            }
412        },
413
414        .grouped_expression => {
415            try walkExpression(w, ast.nodeData(node).node_and_token[0]);
416        },
417
418        .container_decl,
419        .container_decl_trailing,
420        .container_decl_arg,
421        .container_decl_arg_trailing,
422        .container_decl_two,
423        .container_decl_two_trailing,
424        .tagged_union,
425        .tagged_union_trailing,
426        .tagged_union_enum_tag,
427        .tagged_union_enum_tag_trailing,
428        .tagged_union_two,
429        .tagged_union_two_trailing,
430        => {
431            var buf: [2]Ast.Node.Index = undefined;
432            return walkContainerDecl(w, node, ast.fullContainerDecl(&buf, node).?);
433        },
434
435        .error_set_decl => {
436            const lbrace, const rbrace = ast.nodeData(node).token_and_token;
437
438            var i = lbrace + 1;
439            while (i < rbrace) : (i += 1) {
440                switch (ast.tokenTag(i)) {
441                    .doc_comment => unreachable, // TODO
442                    .identifier => try walkIdentifier(w, i),
443                    .comma => {},
444                    else => unreachable,
445                }
446            }
447        },
448
449        .builtin_call_two,
450        .builtin_call_two_comma,
451        .builtin_call,
452        .builtin_call_comma,
453        => {
454            var buf: [2]Ast.Node.Index = undefined;
455            const params = ast.builtinCallParams(&buf, node).?;
456            return walkBuiltinCall(w, node, params);
457        },
458
459        .fn_proto_simple,
460        .fn_proto_multi,
461        .fn_proto_one,
462        .fn_proto,
463        => {
464            var buf: [1]Ast.Node.Index = undefined;
465            return walkFnProto(w, ast.fullFnProto(&buf, node).?);
466        },
467
468        .anyframe_type => {
469            _, const child_type = ast.nodeData(node).token_and_node;
470            return walkExpression(w, child_type);
471        },
472
473        .@"switch",
474        .switch_comma,
475        => {
476            const full = ast.fullSwitch(node).?;
477            try walkExpression(w, full.ast.condition); // condition expression
478            try walkExpressions(w, full.ast.cases);
479        },
480
481        .switch_case_one,
482        .switch_case_inline_one,
483        .switch_case,
484        .switch_case_inline,
485        => return walkSwitchCase(w, ast.fullSwitchCase(node).?),
486
487        .while_simple,
488        .while_cont,
489        .@"while",
490        => return walkWhile(w, node, ast.fullWhile(node).?),
491
492        .for_simple,
493        .@"for",
494        => return walkFor(w, ast.fullFor(node).?),
495
496        .if_simple,
497        .@"if",
498        => return walkIf(w, node, ast.fullIf(node).?),
499
500        .asm_simple,
501        .@"asm",
502        => return walkAsm(w, ast.fullAsm(node).?),
503
504        .asm_legacy => {
505            return walkAsmLegacy(w, ast.legacyAsm(node).?);
506        },
507
508        .enum_literal => {
509            return walkIdentifier(w, ast.nodeMainToken(node)); // name
510        },
511
512        .fn_decl => unreachable,
513        .container_field => unreachable,
514        .container_field_init => unreachable,
515        .container_field_align => unreachable,
516        .root => unreachable,
517        .global_var_decl => unreachable,
518        .local_var_decl => unreachable,
519        .simple_var_decl => unreachable,
520        .aligned_var_decl => unreachable,
521        .test_decl => unreachable,
522        .asm_output => unreachable,
523        .asm_input => unreachable,
524    }
525}
526
527fn walkGlobalVarDecl(w: *Walk, decl_node: Ast.Node.Index, var_decl: Ast.full.VarDecl) Error!void {
528    _ = decl_node;
529
530    if (var_decl.ast.type_node.unwrap()) |type_node| {
531        try walkExpression(w, type_node);
532    }
533
534    if (var_decl.ast.align_node.unwrap()) |align_node| {
535        try walkExpression(w, align_node);
536    }
537
538    if (var_decl.ast.addrspace_node.unwrap()) |addrspace_node| {
539        try walkExpression(w, addrspace_node);
540    }
541
542    if (var_decl.ast.section_node.unwrap()) |section_node| {
543        try walkExpression(w, section_node);
544    }
545
546    if (var_decl.ast.init_node.unwrap()) |init_node| {
547        if (!isUndefinedIdent(w.ast, init_node)) {
548            try w.transformations.append(.{ .replace_with_undef = init_node });
549        }
550        try walkExpression(w, init_node);
551    }
552}
553
554fn walkLocalVarDecl(w: *Walk, var_decl: Ast.full.VarDecl) Error!void {
555    try walkIdentifierNew(w, var_decl.ast.mut_token + 1); // name
556
557    if (var_decl.ast.type_node.unwrap()) |type_node| {
558        try walkExpression(w, type_node);
559    }
560
561    if (var_decl.ast.align_node.unwrap()) |align_node| {
562        try walkExpression(w, align_node);
563    }
564
565    if (var_decl.ast.addrspace_node.unwrap()) |addrspace_node| {
566        try walkExpression(w, addrspace_node);
567    }
568
569    if (var_decl.ast.section_node.unwrap()) |section_node| {
570        try walkExpression(w, section_node);
571    }
572
573    if (var_decl.ast.init_node.unwrap()) |init_node| {
574        if (!isUndefinedIdent(w.ast, init_node)) {
575            try w.transformations.append(.{ .replace_with_undef = init_node });
576        }
577        try walkExpression(w, init_node);
578    }
579}
580
581fn walkContainerField(w: *Walk, field: Ast.full.ContainerField) Error!void {
582    if (field.ast.type_expr.unwrap()) |type_expr| {
583        try walkExpression(w, type_expr); // type
584    }
585    if (field.ast.align_expr.unwrap()) |align_expr| {
586        try walkExpression(w, align_expr); // alignment
587    }
588    if (field.ast.value_expr.unwrap()) |value_expr| {
589        try walkExpression(w, value_expr); // value
590    }
591}
592
593fn walkBlock(
594    w: *Walk,
595    block_node: Ast.Node.Index,
596    statements: []const Ast.Node.Index,
597) Error!void {
598    _ = block_node;
599    const ast = w.ast;
600
601    for (statements) |stmt| {
602        switch (ast.nodeTag(stmt)) {
603            .global_var_decl,
604            .local_var_decl,
605            .simple_var_decl,
606            .aligned_var_decl,
607            => {
608                const var_decl = ast.fullVarDecl(stmt).?;
609                if (var_decl.ast.init_node != .none and
610                    isUndefinedIdent(w.ast, var_decl.ast.init_node.unwrap().?))
611                {
612                    try w.transformations.append(.{ .delete_var_decl = .{
613                        .var_decl_node = stmt,
614                        .references = .{},
615                    } });
616                    const name_tok = var_decl.ast.mut_token + 1;
617                    const name_bytes = ast.tokenSlice(name_tok);
618                    try w.replace_names.put(w.gpa, name_bytes, @intCast(w.transformations.items.len - 1));
619                } else {
620                    try walkLocalVarDecl(w, var_decl);
621                }
622            },
623
624            else => {
625                switch (categorizeStmt(ast, stmt)) {
626                    // Don't try to remove `_ = foo;` discards; those are handled separately.
627                    .discard_identifier => {},
628                    // definitely try to remove `_ = undefined;` though.
629                    .discard_undefined, .trap_call, .other => {
630                        try w.transformations.append(.{ .delete_node = stmt });
631                    },
632                }
633                try walkExpression(w, stmt);
634            },
635        }
636    }
637}
638
639fn walkArrayType(w: *Walk, array_type: Ast.full.ArrayType) Error!void {
640    try walkExpression(w, array_type.ast.elem_count);
641    if (array_type.ast.sentinel.unwrap()) |sentinel| {
642        try walkExpression(w, sentinel);
643    }
644    return walkExpression(w, array_type.ast.elem_type);
645}
646
647fn walkArrayInit(w: *Walk, array_init: Ast.full.ArrayInit) Error!void {
648    if (array_init.ast.type_expr.unwrap()) |type_expr| {
649        try walkExpression(w, type_expr); // T
650    }
651    for (array_init.ast.elements) |elem_init| {
652        try walkExpression(w, elem_init);
653    }
654}
655
656fn walkStructInit(
657    w: *Walk,
658    struct_node: Ast.Node.Index,
659    struct_init: Ast.full.StructInit,
660) Error!void {
661    _ = struct_node;
662    if (struct_init.ast.type_expr.unwrap()) |type_expr| {
663        try walkExpression(w, type_expr); // T
664    }
665    for (struct_init.ast.fields) |field_init| {
666        try walkExpression(w, field_init);
667    }
668}
669
670fn walkCall(w: *Walk, call: Ast.full.Call) Error!void {
671    try walkExpression(w, call.ast.fn_expr);
672    try walkExpressions(w, call.ast.params);
673}
674
675fn walkSlice(
676    w: *Walk,
677    slice_node: Ast.Node.Index,
678    slice: Ast.full.Slice,
679) Error!void {
680    _ = slice_node;
681    try walkExpression(w, slice.ast.sliced);
682    try walkExpression(w, slice.ast.start);
683    if (slice.ast.end.unwrap()) |end| {
684        try walkExpression(w, end);
685    }
686    if (slice.ast.sentinel.unwrap()) |sentinel| {
687        try walkExpression(w, sentinel);
688    }
689}
690
691fn walkIdentifier(w: *Walk, name_ident: Ast.TokenIndex) Error!void {
692    const ast = w.ast;
693    assert(ast.tokenTag(name_ident) == .identifier);
694    const name_bytes = ast.tokenSlice(name_ident);
695    _ = w.unreferenced_globals.swapRemove(name_bytes);
696}
697
698fn walkIdentifierNew(w: *Walk, name_ident: Ast.TokenIndex) Error!void {
699    _ = w;
700    _ = name_ident;
701}
702
703fn walkContainerDecl(
704    w: *Walk,
705    container_decl_node: Ast.Node.Index,
706    container_decl: Ast.full.ContainerDecl,
707) Error!void {
708    _ = container_decl_node;
709    if (container_decl.ast.arg.unwrap()) |arg| {
710        try walkExpression(w, arg);
711    }
712    try walkMembers(w, container_decl.ast.members);
713}
714
715fn walkBuiltinCall(
716    w: *Walk,
717    call_node: Ast.Node.Index,
718    params: []const Ast.Node.Index,
719) Error!void {
720    const ast = w.ast;
721    const builtin_token = ast.nodeMainToken(call_node);
722    const builtin_name = ast.tokenSlice(builtin_token);
723    const info = BuiltinFn.list.get(builtin_name).?;
724    switch (info.tag) {
725        .import => {
726            const operand_node = params[0];
727            const str_lit_token = ast.nodeMainToken(operand_node);
728            const token_bytes = ast.tokenSlice(str_lit_token);
729            if (std.mem.endsWith(u8, token_bytes, ".zig\"")) {
730                const imported_string = std.zig.string_literal.parseAlloc(w.arena, token_bytes) catch
731                    unreachable;
732                try w.transformations.append(.{ .inline_imported_file = .{
733                    .builtin_call_node = call_node,
734                    .imported_string = imported_string,
735                    .in_scope_names = try std.StringArrayHashMapUnmanaged(void).init(
736                        w.arena,
737                        w.in_scope_names.keys(),
738                        &.{},
739                    ),
740                } });
741            }
742        },
743        else => {},
744    }
745    for (params) |param_node| {
746        try walkExpression(w, param_node);
747    }
748}
749
750fn walkFnProto(w: *Walk, fn_proto: Ast.full.FnProto) Error!void {
751    const ast = w.ast;
752
753    {
754        var it = fn_proto.iterate(ast);
755        while (it.next()) |param| {
756            if (param.type_expr) |type_expr| {
757                try walkExpression(w, type_expr);
758            }
759        }
760    }
761
762    if (fn_proto.ast.align_expr.unwrap()) |align_expr| {
763        try walkExpression(w, align_expr);
764    }
765
766    if (fn_proto.ast.addrspace_expr.unwrap()) |addrspace_expr| {
767        try walkExpression(w, addrspace_expr);
768    }
769
770    if (fn_proto.ast.section_expr.unwrap()) |section_expr| {
771        try walkExpression(w, section_expr);
772    }
773
774    if (fn_proto.ast.callconv_expr.unwrap()) |callconv_expr| {
775        try walkExpression(w, callconv_expr);
776    }
777
778    const return_type = fn_proto.ast.return_type.unwrap().?;
779    try walkExpression(w, return_type);
780}
781
782fn walkExpressions(w: *Walk, expressions: []const Ast.Node.Index) Error!void {
783    for (expressions) |expression| {
784        try walkExpression(w, expression);
785    }
786}
787
788fn walkSwitchCase(w: *Walk, switch_case: Ast.full.SwitchCase) Error!void {
789    for (switch_case.ast.values) |value_expr| {
790        try walkExpression(w, value_expr);
791    }
792    try walkExpression(w, switch_case.ast.target_expr);
793}
794
795fn walkWhile(w: *Walk, node_index: Ast.Node.Index, while_node: Ast.full.While) Error!void {
796    // Perform these transformations in this priority order:
797    // 1. If the `else` expression is missing or an empty block, replace the condition with `if (true)` if it is not already.
798    // 2. If the `then` block is empty, replace the condition with `if (false)` if it is not already.
799    // 3. If the condition is `if (true)`, replace the `if` expression with the contents of the `then` expression.
800    // 4. If the condition is `if (false)`, replace the `if` expression with the contents of the `else` expression.
801    if (!isTrueIdent(w.ast, while_node.ast.cond_expr) and
802        (while_node.ast.else_expr == .none or isEmptyBlock(w.ast, while_node.ast.else_expr.unwrap().?)))
803    {
804        try w.transformations.ensureUnusedCapacity(1);
805        w.transformations.appendAssumeCapacity(.{ .replace_with_true = while_node.ast.cond_expr });
806    } else if (!isFalseIdent(w.ast, while_node.ast.cond_expr) and isEmptyBlock(w.ast, while_node.ast.then_expr)) {
807        try w.transformations.ensureUnusedCapacity(1);
808        w.transformations.appendAssumeCapacity(.{ .replace_with_false = while_node.ast.cond_expr });
809    } else if (isTrueIdent(w.ast, while_node.ast.cond_expr)) {
810        try w.transformations.ensureUnusedCapacity(1);
811        w.transformations.appendAssumeCapacity(.{ .replace_node = .{
812            .to_replace = node_index,
813            .replacement = while_node.ast.then_expr,
814        } });
815    } else if (isFalseIdent(w.ast, while_node.ast.cond_expr)) {
816        try w.transformations.ensureUnusedCapacity(1);
817        w.transformations.appendAssumeCapacity(.{ .replace_node = .{
818            .to_replace = node_index,
819            .replacement = while_node.ast.else_expr.unwrap().?,
820        } });
821    }
822
823    try walkExpression(w, while_node.ast.cond_expr); // condition
824
825    if (while_node.ast.cont_expr.unwrap()) |cont_expr| {
826        try walkExpression(w, cont_expr);
827    }
828
829    try walkExpression(w, while_node.ast.then_expr);
830
831    if (while_node.ast.else_expr.unwrap()) |else_expr| {
832        try walkExpression(w, else_expr);
833    }
834}
835
836fn walkFor(w: *Walk, for_node: Ast.full.For) Error!void {
837    try walkExpressions(w, for_node.ast.inputs);
838    try walkExpression(w, for_node.ast.then_expr);
839    if (for_node.ast.else_expr.unwrap()) |else_expr| {
840        try walkExpression(w, else_expr);
841    }
842}
843
844fn walkIf(w: *Walk, node_index: Ast.Node.Index, if_node: Ast.full.If) Error!void {
845    // Perform these transformations in this priority order:
846    // 1. If the `else` expression is missing or an empty block, replace the condition with `if (true)` if it is not already.
847    // 2. If the `then` block is empty, replace the condition with `if (false)` if it is not already.
848    // 3. If the condition is `if (true)`, replace the `if` expression with the contents of the `then` expression.
849    // 4. If the condition is `if (false)`, replace the `if` expression with the contents of the `else` expression.
850    if (!isTrueIdent(w.ast, if_node.ast.cond_expr) and
851        (if_node.ast.else_expr == .none or isEmptyBlock(w.ast, if_node.ast.else_expr.unwrap().?)))
852    {
853        try w.transformations.ensureUnusedCapacity(1);
854        w.transformations.appendAssumeCapacity(.{ .replace_with_true = if_node.ast.cond_expr });
855    } else if (!isFalseIdent(w.ast, if_node.ast.cond_expr) and isEmptyBlock(w.ast, if_node.ast.then_expr)) {
856        try w.transformations.ensureUnusedCapacity(1);
857        w.transformations.appendAssumeCapacity(.{ .replace_with_false = if_node.ast.cond_expr });
858    } else if (isTrueIdent(w.ast, if_node.ast.cond_expr)) {
859        try w.transformations.ensureUnusedCapacity(1);
860        w.transformations.appendAssumeCapacity(.{ .replace_node = .{
861            .to_replace = node_index,
862            .replacement = if_node.ast.then_expr,
863        } });
864    } else if (isFalseIdent(w.ast, if_node.ast.cond_expr)) {
865        try w.transformations.ensureUnusedCapacity(1);
866        w.transformations.appendAssumeCapacity(.{ .replace_node = .{
867            .to_replace = node_index,
868            .replacement = if_node.ast.else_expr.unwrap().?,
869        } });
870    }
871
872    try walkExpression(w, if_node.ast.cond_expr); // condition
873    try walkExpression(w, if_node.ast.then_expr);
874    if (if_node.ast.else_expr.unwrap()) |else_expr| {
875        try walkExpression(w, else_expr);
876    }
877}
878
879fn walkAsm(w: *Walk, asm_node: Ast.full.Asm) Error!void {
880    try walkExpression(w, asm_node.ast.template);
881    try walkExpressions(w, asm_node.ast.items);
882}
883
884fn walkAsmLegacy(w: *Walk, asm_node: Ast.full.AsmLegacy) Error!void {
885    try walkExpression(w, asm_node.ast.template);
886    try walkExpressions(w, asm_node.ast.items);
887}
888
889/// Check if it is already gutted (i.e. its body replaced with `@trap()`).
890fn isFnBodyGutted(ast: *const Ast, body_node: Ast.Node.Index) bool {
891    // skip over discards
892    var statements_buf: [2]Ast.Node.Index = undefined;
893    const statements = switch (ast.nodeTag(body_node)) {
894        .block_two,
895        .block_two_semicolon,
896        .block,
897        .block_semicolon,
898        => ast.blockStatements(&statements_buf, body_node).?,
899
900        else => return false,
901    };
902    var i: usize = 0;
903    while (i < statements.len) : (i += 1) {
904        switch (categorizeStmt(ast, statements[i])) {
905            .discard_identifier => continue,
906            .trap_call => return i + 1 == statements.len,
907            else => return false,
908        }
909    }
910    return false;
911}
912
913const StmtCategory = enum {
914    discard_undefined,
915    discard_identifier,
916    trap_call,
917    other,
918};
919
920fn categorizeStmt(ast: *const Ast, stmt: Ast.Node.Index) StmtCategory {
921    switch (ast.nodeTag(stmt)) {
922        .builtin_call_two,
923        .builtin_call_two_comma,
924        .builtin_call,
925        .builtin_call_comma,
926        => {
927            var buf: [2]Ast.Node.Index = undefined;
928            const params = ast.builtinCallParams(&buf, stmt).?;
929            return categorizeBuiltinCall(ast, ast.nodeMainToken(stmt), params);
930        },
931        .assign => {
932            const lhs, const rhs = ast.nodeData(stmt).node_and_node;
933            if (isDiscardIdent(ast, lhs) and ast.nodeTag(rhs) == .identifier) {
934                const name_bytes = ast.tokenSlice(ast.nodeMainToken(rhs));
935                if (std.mem.eql(u8, name_bytes, "undefined")) {
936                    return .discard_undefined;
937                } else {
938                    return .discard_identifier;
939                }
940            }
941            return .other;
942        },
943        else => return .other,
944    }
945}
946
947fn categorizeBuiltinCall(
948    ast: *const Ast,
949    builtin_token: Ast.TokenIndex,
950    params: []const Ast.Node.Index,
951) StmtCategory {
952    if (params.len != 0) return .other;
953    const name_bytes = ast.tokenSlice(builtin_token);
954    if (std.mem.eql(u8, name_bytes, "@trap"))
955        return .trap_call;
956    return .other;
957}
958
959fn isDiscardIdent(ast: *const Ast, node: Ast.Node.Index) bool {
960    return isMatchingIdent(ast, node, "_");
961}
962
963fn isUndefinedIdent(ast: *const Ast, node: Ast.Node.Index) bool {
964    return isMatchingIdent(ast, node, "undefined");
965}
966
967fn isTrueIdent(ast: *const Ast, node: Ast.Node.Index) bool {
968    return isMatchingIdent(ast, node, "true");
969}
970
971fn isFalseIdent(ast: *const Ast, node: Ast.Node.Index) bool {
972    return isMatchingIdent(ast, node, "false");
973}
974
975fn isMatchingIdent(ast: *const Ast, node: Ast.Node.Index, string: []const u8) bool {
976    switch (ast.nodeTag(node)) {
977        .identifier => {
978            const token_index = ast.nodeMainToken(node);
979            const name_bytes = ast.tokenSlice(token_index);
980            return std.mem.eql(u8, name_bytes, string);
981        },
982        else => return false,
983    }
984}
985
986fn isEmptyBlock(ast: *const Ast, node: Ast.Node.Index) bool {
987    switch (ast.nodeTag(node)) {
988        .block_two => {
989            const opt_lhs, const opt_rhs = ast.nodeData(node).opt_node_and_opt_node;
990            return opt_lhs == .none and opt_rhs == .none;
991        },
992        else => return false,
993    }
994}