master
   1//! Find and annotate identifiers with links to their declarations.
   2
   3const Walk = @This();
   4const std = @import("std");
   5const Ast = std.zig.Ast;
   6const assert = std.debug.assert;
   7const log = std.log;
   8const gpa = std.heap.wasm_allocator;
   9const Oom = error{OutOfMemory};
  10
  11pub const Decl = @import("Decl.zig");
  12
  13pub var files: std.StringArrayHashMapUnmanaged(File) = .empty;
  14pub var decls: std.ArrayList(Decl) = .empty;
  15pub var modules: std.StringArrayHashMapUnmanaged(File.Index) = .empty;
  16
  17file: File.Index,
  18
  19/// keep in sync with "CAT_" constants in main.js
  20pub const Category = union(enum(u8)) {
  21    /// A struct type used only to group declarations.
  22    namespace: Ast.Node.Index,
  23    /// A container type (struct, union, enum, opaque).
  24    container: Ast.Node.Index,
  25    global_variable: Ast.Node.Index,
  26    /// A function that has not been detected as returning a type.
  27    function: Ast.Node.Index,
  28    primitive: Ast.Node.Index,
  29    error_set: Ast.Node.Index,
  30    global_const: Ast.Node.Index,
  31    alias: Decl.Index,
  32    /// A primitive identifier that is also a type.
  33    type,
  34    /// Specifically it is the literal `type`.
  35    type_type,
  36    /// A function that returns a type.
  37    type_function: Ast.Node.Index,
  38
  39    pub const Tag = @typeInfo(Category).@"union".tag_type.?;
  40};
  41
  42pub const File = struct {
  43    ast: Ast,
  44    /// Maps identifiers to the declarations they point to.
  45    ident_decls: std.AutoArrayHashMapUnmanaged(Ast.TokenIndex, Ast.Node.Index) = .empty,
  46    /// Maps field access identifiers to the containing field access node.
  47    token_parents: std.AutoArrayHashMapUnmanaged(Ast.TokenIndex, Ast.Node.Index) = .empty,
  48    /// Maps declarations to their global index.
  49    node_decls: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, Decl.Index) = .empty,
  50    /// Maps function declarations to doctests.
  51    doctests: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, Ast.Node.Index) = .empty,
  52    /// root node => its namespace scope
  53    /// struct/union/enum/opaque decl node => its namespace scope
  54    /// local var decl node => its local variable scope
  55    scopes: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, *Scope) = .empty,
  56
  57    pub fn lookup_token(file: *File, token: Ast.TokenIndex) Decl.Index {
  58        const decl_node = file.ident_decls.get(token) orelse return .none;
  59        return file.node_decls.get(decl_node) orelse return .none;
  60    }
  61
  62    pub const Index = enum(u32) {
  63        _,
  64
  65        fn add_decl(i: Index, node: Ast.Node.Index, parent_decl: Decl.Index) Oom!Decl.Index {
  66            try decls.append(gpa, .{
  67                .ast_node = node,
  68                .file = i,
  69                .parent = parent_decl,
  70            });
  71            const decl_index: Decl.Index = @enumFromInt(decls.items.len - 1);
  72            try i.get().node_decls.put(gpa, node, decl_index);
  73            return decl_index;
  74        }
  75
  76        pub fn get(i: File.Index) *File {
  77            return &files.values()[@intFromEnum(i)];
  78        }
  79
  80        pub fn get_ast(i: File.Index) *Ast {
  81            return &i.get().ast;
  82        }
  83
  84        pub fn path(i: File.Index) []const u8 {
  85            return files.keys()[@intFromEnum(i)];
  86        }
  87
  88        pub fn findRootDecl(file_index: File.Index) Decl.Index {
  89            return file_index.get().node_decls.values()[0];
  90        }
  91
  92        pub fn categorize_decl(file_index: File.Index, node: Ast.Node.Index) Category {
  93            const ast = file_index.get_ast();
  94            switch (ast.nodeTag(node)) {
  95                .root => {
  96                    for (ast.rootDecls()) |member| {
  97                        switch (ast.nodeTag(member)) {
  98                            .container_field_init,
  99                            .container_field_align,
 100                            .container_field,
 101                            => return .{ .container = node },
 102                            else => {},
 103                        }
 104                    }
 105                    return .{ .namespace = node };
 106                },
 107
 108                .global_var_decl,
 109                .local_var_decl,
 110                .simple_var_decl,
 111                .aligned_var_decl,
 112                => {
 113                    const var_decl = ast.fullVarDecl(node).?;
 114                    if (ast.tokenTag(var_decl.ast.mut_token) == .keyword_var)
 115                        return .{ .global_variable = node };
 116                    const init_node = var_decl.ast.init_node.unwrap() orelse
 117                        return .{ .global_const = node };
 118
 119                    return categorize_expr(file_index, init_node);
 120                },
 121
 122                .fn_proto,
 123                .fn_proto_multi,
 124                .fn_proto_one,
 125                .fn_proto_simple,
 126                .fn_decl,
 127                => {
 128                    var buf: [1]Ast.Node.Index = undefined;
 129                    const full = ast.fullFnProto(&buf, node).?;
 130                    return categorize_func(file_index, node, full);
 131                },
 132
 133                else => unreachable,
 134            }
 135        }
 136
 137        pub fn categorize_func(
 138            file_index: File.Index,
 139            node: Ast.Node.Index,
 140            full: Ast.full.FnProto,
 141        ) Category {
 142            return switch (categorize_expr(file_index, full.ast.return_type.unwrap().?)) {
 143                .namespace, .container, .error_set, .type_type => .{ .type_function = node },
 144                else => .{ .function = node },
 145            };
 146        }
 147
 148        pub fn categorize_expr_deep(file_index: File.Index, node: Ast.Node.Index) Category {
 149            return switch (categorize_expr(file_index, node)) {
 150                .alias => |aliasee| aliasee.get().categorize(),
 151                else => |result| result,
 152            };
 153        }
 154
 155        pub fn categorize_expr(file_index: File.Index, node: Ast.Node.Index) Category {
 156            const file = file_index.get();
 157            const ast = file_index.get_ast();
 158            //log.debug("categorize_expr tag {s}", .{@tagName(ast.nodeTag(node))});
 159            return switch (ast.nodeTag(node)) {
 160                .container_decl,
 161                .container_decl_trailing,
 162                .container_decl_arg,
 163                .container_decl_arg_trailing,
 164                .container_decl_two,
 165                .container_decl_two_trailing,
 166                .tagged_union,
 167                .tagged_union_trailing,
 168                .tagged_union_enum_tag,
 169                .tagged_union_enum_tag_trailing,
 170                .tagged_union_two,
 171                .tagged_union_two_trailing,
 172                => {
 173                    var buf: [2]Ast.Node.Index = undefined;
 174                    const container_decl = ast.fullContainerDecl(&buf, node).?;
 175                    if (ast.tokenTag(container_decl.ast.main_token) != .keyword_struct) {
 176                        return .{ .container = node };
 177                    }
 178                    for (container_decl.ast.members) |member| {
 179                        switch (ast.nodeTag(member)) {
 180                            .container_field_init,
 181                            .container_field_align,
 182                            .container_field,
 183                            => return .{ .container = node },
 184                            else => {},
 185                        }
 186                    }
 187                    return .{ .namespace = node };
 188                },
 189
 190                .error_set_decl,
 191                .merge_error_sets,
 192                => .{ .error_set = node },
 193
 194                .identifier => {
 195                    const name_token = ast.nodeMainToken(node);
 196                    const ident_name = ast.tokenSlice(name_token);
 197                    if (std.mem.eql(u8, ident_name, "type"))
 198                        return .type_type;
 199
 200                    if (isPrimitiveNonType(ident_name))
 201                        return .{ .primitive = node };
 202
 203                    if (std.zig.primitives.isPrimitive(ident_name))
 204                        return .type;
 205
 206                    if (file.ident_decls.get(name_token)) |decl_node| {
 207                        const decl_index = file.node_decls.get(decl_node) orelse .none;
 208                        if (decl_index != .none) return .{ .alias = decl_index };
 209                        return categorize_decl(file_index, decl_node);
 210                    }
 211
 212                    return .{ .global_const = node };
 213                },
 214
 215                .field_access => {
 216                    const object_node, const field_ident = ast.nodeData(node).node_and_token;
 217                    const field_name = ast.tokenSlice(field_ident);
 218
 219                    switch (categorize_expr(file_index, object_node)) {
 220                        .alias => |aliasee| if (aliasee.get().get_child(field_name)) |decl_index| {
 221                            return .{ .alias = decl_index };
 222                        },
 223                        else => {},
 224                    }
 225
 226                    return .{ .global_const = node };
 227                },
 228
 229                .builtin_call_two,
 230                .builtin_call_two_comma,
 231                .builtin_call,
 232                .builtin_call_comma,
 233                => {
 234                    var buf: [2]Ast.Node.Index = undefined;
 235                    const params = ast.builtinCallParams(&buf, node).?;
 236                    return categorize_builtin_call(file_index, node, params);
 237                },
 238
 239                .call_one,
 240                .call_one_comma,
 241                .call,
 242                .call_comma,
 243                => {
 244                    var buf: [1]Ast.Node.Index = undefined;
 245                    return categorize_call(file_index, node, ast.fullCall(&buf, node).?);
 246                },
 247
 248                .if_simple,
 249                .@"if",
 250                => {
 251                    const if_full = ast.fullIf(node).?;
 252                    if (if_full.ast.else_expr.unwrap()) |else_expr| {
 253                        const then_cat = categorize_expr_deep(file_index, if_full.ast.then_expr);
 254                        const else_cat = categorize_expr_deep(file_index, else_expr);
 255                        if (then_cat == .type_type and else_cat == .type_type) {
 256                            return .type_type;
 257                        } else if (then_cat == .error_set and else_cat == .error_set) {
 258                            return .{ .error_set = node };
 259                        } else if (then_cat == .type or else_cat == .type or
 260                            then_cat == .namespace or else_cat == .namespace or
 261                            then_cat == .container or else_cat == .container or
 262                            then_cat == .error_set or else_cat == .error_set or
 263                            then_cat == .type_function or else_cat == .type_function)
 264                        {
 265                            return .type;
 266                        }
 267                    }
 268                    return .{ .global_const = node };
 269                },
 270
 271                .@"switch", .switch_comma => return categorize_switch(file_index, node),
 272
 273                .optional_type,
 274                .array_type,
 275                .array_type_sentinel,
 276                .ptr_type_aligned,
 277                .ptr_type_sentinel,
 278                .ptr_type,
 279                .ptr_type_bit_range,
 280                .anyframe_type,
 281                => .type,
 282
 283                else => .{ .global_const = node },
 284            };
 285        }
 286
 287        fn categorize_call(
 288            file_index: File.Index,
 289            node: Ast.Node.Index,
 290            call: Ast.full.Call,
 291        ) Category {
 292            return switch (categorize_expr(file_index, call.ast.fn_expr)) {
 293                .type_function => .type,
 294                .alias => |aliasee| categorize_decl_as_callee(aliasee, node),
 295                else => .{ .global_const = node },
 296            };
 297        }
 298
 299        fn categorize_decl_as_callee(decl_index: Decl.Index, call_node: Ast.Node.Index) Category {
 300            return switch (decl_index.get().categorize()) {
 301                .type_function => .type,
 302                .alias => |aliasee| categorize_decl_as_callee(aliasee, call_node),
 303                else => .{ .global_const = call_node },
 304            };
 305        }
 306
 307        fn categorize_builtin_call(
 308            file_index: File.Index,
 309            node: Ast.Node.Index,
 310            params: []const Ast.Node.Index,
 311        ) Category {
 312            const ast = file_index.get_ast();
 313            const builtin_token = ast.nodeMainToken(node);
 314            const builtin_name = ast.tokenSlice(builtin_token);
 315            if (std.mem.eql(u8, builtin_name, "@import")) {
 316                const str_lit_token = ast.nodeMainToken(params[0]);
 317                const str_bytes = ast.tokenSlice(str_lit_token);
 318                const file_path = std.zig.string_literal.parseAlloc(gpa, str_bytes) catch @panic("OOM");
 319                defer gpa.free(file_path);
 320                if (modules.get(file_path)) |imported_file_index| {
 321                    return .{ .alias = File.Index.findRootDecl(imported_file_index) };
 322                }
 323                const base_path = file_index.path();
 324                const resolved_path = std.fs.path.resolvePosix(gpa, &.{
 325                    base_path, "..", file_path,
 326                }) catch @panic("OOM");
 327                defer gpa.free(resolved_path);
 328                log.debug("from '{s}' @import '{s}' resolved='{s}'", .{
 329                    base_path, file_path, resolved_path,
 330                });
 331                if (files.getIndex(resolved_path)) |imported_file_index| {
 332                    return .{ .alias = File.Index.findRootDecl(@enumFromInt(imported_file_index)) };
 333                } else {
 334                    log.warn("import target '{s}' did not resolve to any file", .{resolved_path});
 335                }
 336            } else if (std.mem.eql(u8, builtin_name, "@This")) {
 337                if (file_index.get().node_decls.get(node)) |decl_index| {
 338                    return .{ .alias = decl_index };
 339                } else {
 340                    log.warn("@This() is missing link to Decl.Index", .{});
 341                }
 342            }
 343
 344            return .{ .global_const = node };
 345        }
 346
 347        fn categorize_switch(file_index: File.Index, node: Ast.Node.Index) Category {
 348            const ast = file_index.get_ast();
 349            const full = ast.fullSwitch(node).?;
 350            var all_type_type = true;
 351            var all_error_set = true;
 352            var any_type = false;
 353            if (full.ast.cases.len == 0) return .{ .global_const = node };
 354            for (full.ast.cases) |case_node| {
 355                const case = ast.fullSwitchCase(case_node).?;
 356                switch (categorize_expr_deep(file_index, case.ast.target_expr)) {
 357                    .type_type => {
 358                        any_type = true;
 359                        all_error_set = false;
 360                    },
 361                    .error_set => {
 362                        any_type = true;
 363                        all_type_type = false;
 364                    },
 365                    .type, .namespace, .container, .type_function => {
 366                        any_type = true;
 367                        all_error_set = false;
 368                        all_type_type = false;
 369                    },
 370                    else => {
 371                        all_error_set = false;
 372                        all_type_type = false;
 373                    },
 374                }
 375            }
 376            if (all_type_type) return .type_type;
 377            if (all_error_set) return .{ .error_set = node };
 378            if (any_type) return .type;
 379            return .{ .global_const = node };
 380        }
 381    };
 382};
 383
 384pub const ModuleIndex = enum(u32) {
 385    _,
 386};
 387
 388pub fn add_file(file_name: []const u8, bytes: []u8) !File.Index {
 389    const ast = try parse(file_name, bytes);
 390    assert(ast.errors.len == 0);
 391    const file_index: File.Index = @enumFromInt(files.entries.len);
 392    try files.put(gpa, file_name, .{ .ast = ast });
 393
 394    var w: Walk = .{
 395        .file = file_index,
 396    };
 397    const scope = try gpa.create(Scope);
 398    scope.* = .{ .tag = .top };
 399
 400    const decl_index = try file_index.add_decl(.root, .none);
 401    try struct_decl(&w, scope, decl_index, .root, ast.containerDeclRoot());
 402
 403    const file = file_index.get();
 404    shrinkToFit(&file.ident_decls);
 405    shrinkToFit(&file.token_parents);
 406    shrinkToFit(&file.node_decls);
 407    shrinkToFit(&file.doctests);
 408    shrinkToFit(&file.scopes);
 409
 410    return file_index;
 411}
 412
 413/// Parses a file and returns its `Ast`. If the file cannot be parsed, returns
 414/// the `Ast` of an empty file, so that the rest of the Autodoc logic does not
 415/// need to handle parse errors.
 416fn parse(file_name: []const u8, source: []u8) Oom!Ast {
 417    // Require every source file to end with a newline so that Zig's tokenizer
 418    // can continue to require null termination and Autodoc implementation can
 419    // avoid copying source bytes from the decompressed tar file buffer.
 420    const adjusted_source: [:0]const u8 = s: {
 421        if (source.len == 0)
 422            break :s "";
 423        if (source[source.len - 1] != '\n') {
 424            log.err("{s}: expected newline at end of file", .{file_name});
 425            break :s "";
 426        }
 427        source[source.len - 1] = 0;
 428        break :s source[0 .. source.len - 1 :0];
 429    };
 430
 431    var ast = try Ast.parse(gpa, adjusted_source, .zig);
 432    if (ast.errors.len > 0) {
 433        defer ast.deinit(gpa);
 434
 435        const token_offsets = ast.tokens.items(.start);
 436        var rendered_err: std.Io.Writer.Allocating = .init(gpa);
 437        defer rendered_err.deinit();
 438        for (ast.errors) |err| {
 439            const err_offset = token_offsets[err.token] + ast.errorOffset(err);
 440            const err_loc = std.zig.findLineColumn(ast.source, err_offset);
 441            rendered_err.clearRetainingCapacity();
 442            ast.renderError(err, &rendered_err.writer) catch |e| switch (e) {
 443                error.WriteFailed => return error.OutOfMemory,
 444            };
 445            log.err("{s}:{d}:{d}: {s}", .{
 446                file_name, err_loc.line + 1, err_loc.column + 1, rendered_err.written(),
 447            });
 448        }
 449        return Ast.parse(gpa, "", .zig);
 450    }
 451    return ast;
 452}
 453
 454pub const Scope = struct {
 455    tag: Tag,
 456
 457    const Tag = enum { top, local, namespace };
 458
 459    const Local = struct {
 460        base: Scope = .{ .tag = .local },
 461        parent: *Scope,
 462        var_node: Ast.Node.Index,
 463    };
 464
 465    const Namespace = struct {
 466        base: Scope = .{ .tag = .namespace },
 467        parent: *Scope,
 468        names: std.StringArrayHashMapUnmanaged(Ast.Node.Index) = .empty,
 469        doctests: std.StringArrayHashMapUnmanaged(Ast.Node.Index) = .empty,
 470        decl_index: Decl.Index,
 471    };
 472
 473    fn getNamespaceDecl(start_scope: *Scope) Decl.Index {
 474        var it: *Scope = start_scope;
 475        while (true) switch (it.tag) {
 476            .top => unreachable,
 477            .local => {
 478                const local: *Local = @alignCast(@fieldParentPtr("base", it));
 479                it = local.parent;
 480            },
 481            .namespace => {
 482                const namespace: *Namespace = @alignCast(@fieldParentPtr("base", it));
 483                return namespace.decl_index;
 484            },
 485        };
 486    }
 487
 488    pub fn get_child(scope: *Scope, name: []const u8) ?Ast.Node.Index {
 489        switch (scope.tag) {
 490            .top, .local => return null,
 491            .namespace => {
 492                const namespace: *Namespace = @alignCast(@fieldParentPtr("base", scope));
 493                return namespace.names.get(name);
 494            },
 495        }
 496    }
 497
 498    pub fn lookup(start_scope: *Scope, ast: *const Ast, name: []const u8) ?Ast.Node.Index {
 499        var it: *Scope = start_scope;
 500        while (true) switch (it.tag) {
 501            .top => break,
 502            .local => {
 503                const local: *Local = @alignCast(@fieldParentPtr("base", it));
 504                const name_token = ast.nodeMainToken(local.var_node) + 1;
 505                const ident_name = ast.tokenSlice(name_token);
 506                if (std.mem.eql(u8, ident_name, name)) {
 507                    return local.var_node;
 508                }
 509                it = local.parent;
 510            },
 511            .namespace => {
 512                const namespace: *Namespace = @alignCast(@fieldParentPtr("base", it));
 513                if (namespace.names.get(name)) |node| {
 514                    return node;
 515                }
 516                it = namespace.parent;
 517            },
 518        };
 519        return null;
 520    }
 521};
 522
 523fn struct_decl(
 524    w: *Walk,
 525    scope: *Scope,
 526    parent_decl: Decl.Index,
 527    node: Ast.Node.Index,
 528    container_decl: Ast.full.ContainerDecl,
 529) Oom!void {
 530    const ast = w.file.get_ast();
 531
 532    const namespace = try gpa.create(Scope.Namespace);
 533    namespace.* = .{
 534        .parent = scope,
 535        .decl_index = parent_decl,
 536    };
 537    try w.file.get().scopes.putNoClobber(gpa, node, &namespace.base);
 538    try w.scanDecls(namespace, container_decl.ast.members);
 539
 540    for (container_decl.ast.members) |member| switch (ast.nodeTag(member)) {
 541        .container_field_init,
 542        .container_field_align,
 543        .container_field,
 544        => try w.container_field(&namespace.base, parent_decl, ast.fullContainerField(member).?),
 545
 546        .fn_proto,
 547        .fn_proto_multi,
 548        .fn_proto_one,
 549        .fn_proto_simple,
 550        .fn_decl,
 551        => {
 552            var buf: [1]Ast.Node.Index = undefined;
 553            const full = ast.fullFnProto(&buf, member).?;
 554            const fn_name_token = full.ast.fn_token + 1;
 555            const fn_name = ast.tokenSlice(fn_name_token);
 556            if (namespace.doctests.get(fn_name)) |doctest_node| {
 557                try w.file.get().doctests.put(gpa, member, doctest_node);
 558            }
 559            const decl_index = try w.file.add_decl(member, parent_decl);
 560            const body = if (ast.nodeTag(member) == .fn_decl) ast.nodeData(member).node_and_node[1].toOptional() else .none;
 561            try w.fn_decl(&namespace.base, decl_index, body, full);
 562        },
 563
 564        .global_var_decl,
 565        .local_var_decl,
 566        .simple_var_decl,
 567        .aligned_var_decl,
 568        => {
 569            const decl_index = try w.file.add_decl(member, parent_decl);
 570            try w.global_var_decl(&namespace.base, decl_index, ast.fullVarDecl(member).?);
 571        },
 572
 573        .@"comptime",
 574        => try w.expr(&namespace.base, parent_decl, ast.nodeData(member).node),
 575
 576        .test_decl => try w.expr(&namespace.base, parent_decl, ast.nodeData(member).opt_token_and_node[1]),
 577
 578        else => unreachable,
 579    };
 580}
 581
 582fn comptime_decl(
 583    w: *Walk,
 584    scope: *Scope,
 585    parent_decl: Decl.Index,
 586    full: Ast.full.VarDecl,
 587) Oom!void {
 588    try w.expr(scope, parent_decl, full.ast.type_node);
 589    try w.maybe_expr(scope, parent_decl, full.ast.align_node);
 590    try w.maybe_expr(scope, parent_decl, full.ast.addrspace_node);
 591    try w.maybe_expr(scope, parent_decl, full.ast.section_node);
 592    try w.expr(scope, parent_decl, full.ast.init_node);
 593}
 594
 595fn global_var_decl(
 596    w: *Walk,
 597    scope: *Scope,
 598    parent_decl: Decl.Index,
 599    full: Ast.full.VarDecl,
 600) Oom!void {
 601    try w.maybe_expr(scope, parent_decl, full.ast.type_node);
 602    try w.maybe_expr(scope, parent_decl, full.ast.align_node);
 603    try w.maybe_expr(scope, parent_decl, full.ast.addrspace_node);
 604    try w.maybe_expr(scope, parent_decl, full.ast.section_node);
 605    try w.maybe_expr(scope, parent_decl, full.ast.init_node);
 606}
 607
 608fn container_field(
 609    w: *Walk,
 610    scope: *Scope,
 611    parent_decl: Decl.Index,
 612    full: Ast.full.ContainerField,
 613) Oom!void {
 614    try w.maybe_expr(scope, parent_decl, full.ast.type_expr);
 615    try w.maybe_expr(scope, parent_decl, full.ast.align_expr);
 616    try w.maybe_expr(scope, parent_decl, full.ast.value_expr);
 617}
 618
 619fn fn_decl(
 620    w: *Walk,
 621    scope: *Scope,
 622    parent_decl: Decl.Index,
 623    body: Ast.Node.OptionalIndex,
 624    full: Ast.full.FnProto,
 625) Oom!void {
 626    for (full.ast.params) |param| {
 627        try expr(w, scope, parent_decl, param);
 628    }
 629    try expr(w, scope, parent_decl, full.ast.return_type.unwrap().?);
 630    try maybe_expr(w, scope, parent_decl, full.ast.align_expr);
 631    try maybe_expr(w, scope, parent_decl, full.ast.addrspace_expr);
 632    try maybe_expr(w, scope, parent_decl, full.ast.section_expr);
 633    try maybe_expr(w, scope, parent_decl, full.ast.callconv_expr);
 634    try maybe_expr(w, scope, parent_decl, body);
 635}
 636
 637fn maybe_expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, node: Ast.Node.OptionalIndex) Oom!void {
 638    if (node.unwrap()) |n| return expr(w, scope, parent_decl, n);
 639}
 640
 641fn expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, node: Ast.Node.Index) Oom!void {
 642    const ast = w.file.get_ast();
 643    switch (ast.nodeTag(node)) {
 644        .root => unreachable, // Top-level declaration.
 645        .test_decl => unreachable, // Top-level declaration.
 646        .container_field_init => unreachable, // Top-level declaration.
 647        .container_field_align => unreachable, // Top-level declaration.
 648        .container_field => unreachable, // Top-level declaration.
 649        .fn_decl => unreachable, // Top-level declaration.
 650
 651        .global_var_decl => unreachable, // Handled in `block`.
 652        .local_var_decl => unreachable, // Handled in `block`.
 653        .simple_var_decl => unreachable, // Handled in `block`.
 654        .aligned_var_decl => unreachable, // Handled in `block`.
 655        .@"defer" => unreachable, // Handled in `block`.
 656        .@"errdefer" => unreachable, // Handled in `block`.
 657
 658        .switch_case => unreachable, // Handled in `switchExpr`.
 659        .switch_case_inline => unreachable, // Handled in `switchExpr`.
 660        .switch_case_one => unreachable, // Handled in `switchExpr`.
 661        .switch_case_inline_one => unreachable, // Handled in `switchExpr`.
 662
 663        .asm_output => unreachable, // Handled in `asmExpr`.
 664        .asm_input => unreachable, // Handled in `asmExpr`.
 665
 666        .for_range => unreachable, // Handled in `forExpr`.
 667
 668        .assign,
 669        .assign_shl,
 670        .assign_shl_sat,
 671        .assign_shr,
 672        .assign_bit_and,
 673        .assign_bit_or,
 674        .assign_bit_xor,
 675        .assign_div,
 676        .assign_sub,
 677        .assign_sub_wrap,
 678        .assign_sub_sat,
 679        .assign_mod,
 680        .assign_add,
 681        .assign_add_wrap,
 682        .assign_add_sat,
 683        .assign_mul,
 684        .assign_mul_wrap,
 685        .assign_mul_sat,
 686        .shl,
 687        .shr,
 688        .add,
 689        .add_wrap,
 690        .add_sat,
 691        .sub,
 692        .sub_wrap,
 693        .sub_sat,
 694        .mul,
 695        .mul_wrap,
 696        .mul_sat,
 697        .div,
 698        .mod,
 699        .shl_sat,
 700
 701        .bit_and,
 702        .bit_or,
 703        .bit_xor,
 704        .bang_equal,
 705        .equal_equal,
 706        .greater_than,
 707        .greater_or_equal,
 708        .less_than,
 709        .less_or_equal,
 710        .array_cat,
 711
 712        .array_mult,
 713        .error_union,
 714        .merge_error_sets,
 715        .bool_and,
 716        .bool_or,
 717        .@"catch",
 718        .@"orelse",
 719        .array_type,
 720        .array_access,
 721        .switch_range,
 722        => {
 723            const lhs, const rhs = ast.nodeData(node).node_and_node;
 724            try expr(w, scope, parent_decl, lhs);
 725            try expr(w, scope, parent_decl, rhs);
 726        },
 727
 728        .assign_destructure => {
 729            const full = ast.assignDestructure(node);
 730            for (full.ast.variables) |variable_node| try expr(w, scope, parent_decl, variable_node);
 731            _ = try expr(w, scope, parent_decl, full.ast.value_expr);
 732        },
 733
 734        .bool_not,
 735        .bit_not,
 736        .negation,
 737        .negation_wrap,
 738        .deref,
 739        .address_of,
 740        .optional_type,
 741        .@"comptime",
 742        .@"nosuspend",
 743        .@"suspend",
 744        .@"resume",
 745        .@"try",
 746        => try expr(w, scope, parent_decl, ast.nodeData(node).node),
 747        .unwrap_optional,
 748        .grouped_expression,
 749        => try expr(w, scope, parent_decl, ast.nodeData(node).node_and_token[0]),
 750        .@"return" => try maybe_expr(w, scope, parent_decl, ast.nodeData(node).opt_node),
 751
 752        .anyframe_type => try expr(w, scope, parent_decl, ast.nodeData(node).token_and_node[1]),
 753        .@"break" => try maybe_expr(w, scope, parent_decl, ast.nodeData(node).opt_token_and_opt_node[1]),
 754
 755        .identifier => {
 756            const ident_token = ast.nodeMainToken(node);
 757            const ident_name = ast.tokenSlice(ident_token);
 758            if (scope.lookup(ast, ident_name)) |var_node| {
 759                try w.file.get().ident_decls.put(gpa, ident_token, var_node);
 760            }
 761        },
 762        .field_access => {
 763            const object_node, const field_ident = ast.nodeData(node).node_and_token;
 764            try w.file.get().token_parents.put(gpa, field_ident, node);
 765            // This will populate the left-most field object if it is an
 766            // identifier, allowing rendering code to piece together the link.
 767            try expr(w, scope, parent_decl, object_node);
 768        },
 769
 770        .string_literal,
 771        .multiline_string_literal,
 772        .number_literal,
 773        .unreachable_literal,
 774        .enum_literal,
 775        .error_value,
 776        .anyframe_literal,
 777        .@"continue",
 778        .char_literal,
 779        .error_set_decl,
 780        => {},
 781
 782        .asm_simple,
 783        .@"asm",
 784        => {
 785            const full = ast.fullAsm(node).?;
 786            for (full.ast.items) |n| {
 787                // There is a missing call here to expr() for .asm_input and
 788                // .asm_output nodes.
 789                _ = n;
 790            }
 791            try expr(w, scope, parent_decl, full.ast.template);
 792        },
 793
 794        .asm_legacy => {},
 795
 796        .builtin_call_two,
 797        .builtin_call_two_comma,
 798        .builtin_call,
 799        .builtin_call_comma,
 800        => {
 801            var buf: [2]Ast.Node.Index = undefined;
 802            const params = ast.builtinCallParams(&buf, node).?;
 803            return builtin_call(w, scope, parent_decl, node, params);
 804        },
 805
 806        .call_one,
 807        .call_one_comma,
 808        .call,
 809        .call_comma,
 810        => {
 811            var buf: [1]Ast.Node.Index = undefined;
 812            const full = ast.fullCall(&buf, node).?;
 813            try expr(w, scope, parent_decl, full.ast.fn_expr);
 814            for (full.ast.params) |param| {
 815                try expr(w, scope, parent_decl, param);
 816            }
 817        },
 818
 819        .if_simple,
 820        .@"if",
 821        => {
 822            const full = ast.fullIf(node).?;
 823            try expr(w, scope, parent_decl, full.ast.cond_expr);
 824            try expr(w, scope, parent_decl, full.ast.then_expr);
 825            try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
 826        },
 827
 828        .while_simple,
 829        .while_cont,
 830        .@"while",
 831        => {
 832            try while_expr(w, scope, parent_decl, ast.fullWhile(node).?);
 833        },
 834
 835        .for_simple, .@"for" => {
 836            const full = ast.fullFor(node).?;
 837            for (full.ast.inputs) |input| {
 838                if (ast.nodeTag(input) == .for_range) {
 839                    const start, const end = ast.nodeData(input).node_and_opt_node;
 840                    try expr(w, scope, parent_decl, start);
 841                    try maybe_expr(w, scope, parent_decl, end);
 842                } else {
 843                    try expr(w, scope, parent_decl, input);
 844                }
 845            }
 846            try expr(w, scope, parent_decl, full.ast.then_expr);
 847            try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
 848        },
 849
 850        .slice => return slice(w, scope, parent_decl, ast.slice(node)),
 851        .slice_open => return slice(w, scope, parent_decl, ast.sliceOpen(node)),
 852        .slice_sentinel => return slice(w, scope, parent_decl, ast.sliceSentinel(node)),
 853
 854        .block_two,
 855        .block_two_semicolon,
 856        .block,
 857        .block_semicolon,
 858        => {
 859            var buf: [2]Ast.Node.Index = undefined;
 860            const statements = ast.blockStatements(&buf, node).?;
 861            return block(w, scope, parent_decl, statements);
 862        },
 863
 864        .ptr_type_aligned,
 865        .ptr_type_sentinel,
 866        .ptr_type,
 867        .ptr_type_bit_range,
 868        => {
 869            const full = ast.fullPtrType(node).?;
 870            try maybe_expr(w, scope, parent_decl, full.ast.align_node);
 871            try maybe_expr(w, scope, parent_decl, full.ast.addrspace_node);
 872            try maybe_expr(w, scope, parent_decl, full.ast.sentinel);
 873            try maybe_expr(w, scope, parent_decl, full.ast.bit_range_start);
 874            try maybe_expr(w, scope, parent_decl, full.ast.bit_range_end);
 875            try expr(w, scope, parent_decl, full.ast.child_type);
 876        },
 877
 878        .container_decl,
 879        .container_decl_trailing,
 880        .container_decl_arg,
 881        .container_decl_arg_trailing,
 882        .container_decl_two,
 883        .container_decl_two_trailing,
 884        .tagged_union,
 885        .tagged_union_trailing,
 886        .tagged_union_enum_tag,
 887        .tagged_union_enum_tag_trailing,
 888        .tagged_union_two,
 889        .tagged_union_two_trailing,
 890        => {
 891            var buf: [2]Ast.Node.Index = undefined;
 892            return struct_decl(w, scope, parent_decl, node, ast.fullContainerDecl(&buf, node).?);
 893        },
 894
 895        .array_type_sentinel => {
 896            const len_expr, const extra_index = ast.nodeData(node).node_and_extra;
 897            const extra = ast.extraData(extra_index, Ast.Node.ArrayTypeSentinel);
 898            try expr(w, scope, parent_decl, len_expr);
 899            try expr(w, scope, parent_decl, extra.elem_type);
 900            try expr(w, scope, parent_decl, extra.sentinel);
 901        },
 902        .@"switch", .switch_comma => {
 903            const full = ast.fullSwitch(node).?;
 904            try expr(w, scope, parent_decl, full.ast.condition);
 905            for (full.ast.cases) |case_node| {
 906                const case = ast.fullSwitchCase(case_node).?;
 907                for (case.ast.values) |value_node| {
 908                    try expr(w, scope, parent_decl, value_node);
 909                }
 910                try expr(w, scope, parent_decl, case.ast.target_expr);
 911            }
 912        },
 913
 914        .array_init_one,
 915        .array_init_one_comma,
 916        .array_init_dot_two,
 917        .array_init_dot_two_comma,
 918        .array_init_dot,
 919        .array_init_dot_comma,
 920        .array_init,
 921        .array_init_comma,
 922        => {
 923            var buf: [2]Ast.Node.Index = undefined;
 924            const full = ast.fullArrayInit(&buf, node).?;
 925            try maybe_expr(w, scope, parent_decl, full.ast.type_expr);
 926            for (full.ast.elements) |elem| {
 927                try expr(w, scope, parent_decl, elem);
 928            }
 929        },
 930
 931        .struct_init_one,
 932        .struct_init_one_comma,
 933        .struct_init_dot_two,
 934        .struct_init_dot_two_comma,
 935        .struct_init_dot,
 936        .struct_init_dot_comma,
 937        .struct_init,
 938        .struct_init_comma,
 939        => {
 940            var buf: [2]Ast.Node.Index = undefined;
 941            const full = ast.fullStructInit(&buf, node).?;
 942            try maybe_expr(w, scope, parent_decl, full.ast.type_expr);
 943            for (full.ast.fields) |field| {
 944                try expr(w, scope, parent_decl, field);
 945            }
 946        },
 947
 948        .fn_proto_simple,
 949        .fn_proto_multi,
 950        .fn_proto_one,
 951        .fn_proto,
 952        => {
 953            var buf: [1]Ast.Node.Index = undefined;
 954            return fn_decl(w, scope, parent_decl, .none, ast.fullFnProto(&buf, node).?);
 955        },
 956    }
 957}
 958
 959fn slice(w: *Walk, scope: *Scope, parent_decl: Decl.Index, full: Ast.full.Slice) Oom!void {
 960    try expr(w, scope, parent_decl, full.ast.sliced);
 961    try expr(w, scope, parent_decl, full.ast.start);
 962    try maybe_expr(w, scope, parent_decl, full.ast.end);
 963    try maybe_expr(w, scope, parent_decl, full.ast.sentinel);
 964}
 965
 966fn builtin_call(
 967    w: *Walk,
 968    scope: *Scope,
 969    parent_decl: Decl.Index,
 970    node: Ast.Node.Index,
 971    params: []const Ast.Node.Index,
 972) Oom!void {
 973    const ast = w.file.get_ast();
 974    const builtin_token = ast.nodeMainToken(node);
 975    const builtin_name = ast.tokenSlice(builtin_token);
 976    if (std.mem.eql(u8, builtin_name, "@This")) {
 977        try w.file.get().node_decls.put(gpa, node, scope.getNamespaceDecl());
 978    }
 979
 980    for (params) |param| {
 981        try expr(w, scope, parent_decl, param);
 982    }
 983}
 984
 985fn block(
 986    w: *Walk,
 987    parent_scope: *Scope,
 988    parent_decl: Decl.Index,
 989    statements: []const Ast.Node.Index,
 990) Oom!void {
 991    const ast = w.file.get_ast();
 992
 993    var scope = parent_scope;
 994
 995    for (statements) |node| {
 996        switch (ast.nodeTag(node)) {
 997            .global_var_decl,
 998            .local_var_decl,
 999            .simple_var_decl,
1000            .aligned_var_decl,
1001            => {
1002                const full = ast.fullVarDecl(node).?;
1003                try global_var_decl(w, scope, parent_decl, full);
1004                const local = try gpa.create(Scope.Local);
1005                local.* = .{
1006                    .parent = scope,
1007                    .var_node = node,
1008                };
1009                try w.file.get().scopes.putNoClobber(gpa, node, &local.base);
1010                scope = &local.base;
1011            },
1012
1013            .assign_destructure => {
1014                log.debug("walk assign_destructure not implemented yet", .{});
1015            },
1016
1017            .grouped_expression => try expr(w, scope, parent_decl, ast.nodeData(node).node_and_token[0]),
1018
1019            .@"defer" => try expr(w, scope, parent_decl, ast.nodeData(node).node),
1020            .@"errdefer" => try expr(w, scope, parent_decl, ast.nodeData(node).opt_token_and_node[1]),
1021
1022            else => try expr(w, scope, parent_decl, node),
1023        }
1024    }
1025}
1026
1027fn while_expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, full: Ast.full.While) Oom!void {
1028    try expr(w, scope, parent_decl, full.ast.cond_expr);
1029    try maybe_expr(w, scope, parent_decl, full.ast.cont_expr);
1030    try expr(w, scope, parent_decl, full.ast.then_expr);
1031    try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
1032}
1033
1034fn scanDecls(w: *Walk, namespace: *Scope.Namespace, members: []const Ast.Node.Index) Oom!void {
1035    const ast = w.file.get_ast();
1036
1037    for (members) |member_node| {
1038        const name_token = switch (ast.nodeTag(member_node)) {
1039            .global_var_decl,
1040            .local_var_decl,
1041            .simple_var_decl,
1042            .aligned_var_decl,
1043            => ast.nodeMainToken(member_node) + 1,
1044
1045            .fn_proto_simple,
1046            .fn_proto_multi,
1047            .fn_proto_one,
1048            .fn_proto,
1049            .fn_decl,
1050            => blk: {
1051                const ident = ast.nodeMainToken(member_node) + 1;
1052                if (ast.tokenTag(ident) != .identifier) continue;
1053                break :blk ident;
1054            },
1055
1056            .test_decl => {
1057                const opt_ident_token = ast.nodeData(member_node).opt_token_and_node[0];
1058                if (opt_ident_token.unwrap()) |ident_token| {
1059                    const is_doctest = ast.tokenTag(ident_token) == .identifier;
1060                    if (is_doctest) {
1061                        const token_bytes = ast.tokenSlice(ident_token);
1062                        try namespace.doctests.put(gpa, token_bytes, member_node);
1063                    }
1064                }
1065                continue;
1066            },
1067
1068            else => continue,
1069        };
1070
1071        const token_bytes = ast.tokenSlice(name_token);
1072        try namespace.names.put(gpa, token_bytes, member_node);
1073    }
1074}
1075
1076pub fn isPrimitiveNonType(name: []const u8) bool {
1077    return std.mem.eql(u8, name, "undefined") or
1078        std.mem.eql(u8, name, "null") or
1079        std.mem.eql(u8, name, "true") or
1080        std.mem.eql(u8, name, "false");
1081}
1082
1083//test {
1084//    const gpa = std.testing.allocator;
1085//
1086//    var arena_instance = std.heap.ArenaAllocator.init(gpa);
1087//    defer arena_instance.deinit();
1088//    const arena = arena_instance.allocator();
1089//
1090//    // example test command:
1091//    // zig test --dep input.zig -Mroot=src/Walk.zig -Minput.zig=/home/andy/dev/zig/lib/std/fs/File/zig
1092//    var ast = try Ast.parse(gpa, @embedFile("input.zig"), .zig);
1093//    defer ast.deinit(gpa);
1094//
1095//    var w: Walk = .{
1096//        .arena = arena,
1097//        .token_links = .{},
1098//        .ast = &ast,
1099//    };
1100//
1101//    try w.root();
1102//}
1103
1104fn shrinkToFit(m: anytype) void {
1105    m.shrinkAndFree(gpa, m.entries.len);
1106}