master
  1//! This is a simple TCP server which exposes a REPL useful for debugging incremental compilation
  2//! issues. Eventually, this logic should move into `std.zig.Client`/`std.zig.Server` or something
  3//! similar, but for now, this works. The server is enabled by the '--debug-incremental' CLI flag.
  4//! The easiest way to interact with the REPL is to use `telnet`:
  5//! ```
  6//! telnet "::1" 7623
  7//! ```
  8//! 'help' will list available commands. When the debug server is enabled, the compiler tracks a lot
  9//! of extra state (see `Zcu.IncrementalDebugState`), so note that RSS will be higher than usual.
 10
 11comptime {
 12    // This file should only be referenced when debug extensions are enabled.
 13    std.debug.assert(@import("build_options").enable_debug_extensions and !@import("builtin").single_threaded);
 14}
 15
 16zcu: *Zcu,
 17thread: ?std.Thread,
 18running: std.atomic.Value(bool),
 19/// Held by our owner when an update is in-progress, and held by us when responding to a command.
 20/// So, essentially guards all access to `Compilation`, including `Zcu`.
 21mutex: std.Thread.Mutex,
 22
 23pub fn init(zcu: *Zcu) IncrementalDebugServer {
 24    return .{
 25        .zcu = zcu,
 26        .thread = null,
 27        .running = .init(true),
 28        .mutex = .{},
 29    };
 30}
 31
 32pub fn deinit(ids: *IncrementalDebugServer) void {
 33    if (ids.thread) |t| {
 34        ids.running.store(false, .monotonic);
 35        t.join();
 36    }
 37}
 38
 39const port = 7623;
 40pub fn spawn(ids: *IncrementalDebugServer) void {
 41    std.debug.print("spawning incremental debug server on port {d}\n", .{port});
 42    ids.thread = std.Thread.spawn(.{ .allocator = ids.zcu.comp.arena }, runThread, .{ids}) catch |err|
 43        std.process.fatal("failed to spawn incremental debug server: {s}", .{@errorName(err)});
 44}
 45fn runThread(ids: *IncrementalDebugServer) void {
 46    const gpa = ids.zcu.gpa;
 47    const io = ids.zcu.comp.io;
 48
 49    var cmd_buf: [1024]u8 = undefined;
 50    var text_out: std.ArrayList(u8) = .empty;
 51    defer text_out.deinit(gpa);
 52
 53    const addr: std.Io.net.IpAddress = .{ .ip6 = .loopback(port) };
 54    var server = addr.listen(io, .{}) catch @panic("IncrementalDebugServer: failed to listen");
 55    defer server.deinit(io);
 56    var stream = server.accept(io) catch @panic("IncrementalDebugServer: failed to accept");
 57    defer stream.close(io);
 58
 59    var stream_reader = stream.reader(io, &cmd_buf);
 60    var stream_writer = stream.writer(io, &.{});
 61
 62    while (ids.running.load(.monotonic)) {
 63        stream_writer.interface.writeAll("zig> ") catch @panic("IncrementalDebugServer: failed to write");
 64        const untrimmed = stream_reader.interface.takeSentinel('\n') catch |err| switch (err) {
 65            error.EndOfStream => break,
 66            else => @panic("IncrementalDebugServer: failed to read command"),
 67        };
 68        const cmd_and_arg = std.mem.trim(u8, untrimmed, " \t\r\n");
 69        const cmd: []const u8, const arg: []const u8 = if (std.mem.indexOfScalar(u8, cmd_and_arg, ' ')) |i|
 70            .{ cmd_and_arg[0..i], cmd_and_arg[i + 1 ..] }
 71        else
 72            .{ cmd_and_arg, "" };
 73
 74        text_out.clearRetainingCapacity();
 75        {
 76            if (!ids.mutex.tryLock()) {
 77                stream_writer.interface.writeAll("waiting for in-progress update to finish...\n") catch @panic("IncrementalDebugServer: failed to write");
 78                ids.mutex.lock();
 79            }
 80            defer ids.mutex.unlock();
 81            var allocating: std.Io.Writer.Allocating = .fromArrayList(gpa, &text_out);
 82            defer text_out = allocating.toArrayList();
 83            handleCommand(ids.zcu, &allocating.writer, cmd, arg) catch @panic("IncrementalDebugServer: out of memory");
 84        }
 85        text_out.append(gpa, '\n') catch @panic("IncrementalDebugServer: out of memory");
 86        stream_writer.interface.writeAll(text_out.items) catch @panic("IncrementalDebugServer: failed to write");
 87    }
 88    std.debug.print("closing incremental debug server\n", .{});
 89}
 90
 91const help_str: []const u8 =
 92    \\[str] arguments are any string.
 93    \\[id] arguments are a numeric ID/index, like an InternPool index.
 94    \\[unit] arguments are strings like 'func 1234' where '1234' is the relevant index (in this case an InternPool index).
 95    \\
 96    \\MISC
 97    \\  summary
 98    \\    Dump some information about the whole ZCU.
 99    \\  nav_info [id]
100    \\    Dump basic info about a NAV.
101    \\
102    \\SEARCHING
103    \\  find_type [str]
104    \\    Find types (including dead ones) whose names contain the given substring.
105    \\    Starting with '^' or ending with '$' anchors to the start/end of the name.
106    \\  find_nav [str]
107    \\    Find NAVs (including dead ones) whose names contain the given substring.
108    \\    Starting with '^' or ending with '$' anchors to the start/end of the name.
109    \\
110    \\UNITS
111    \\  unit_info [unit]
112    \\    Dump basic info about an analysis unit.
113    \\  unit_dependencies [unit]
114    \\    List all units which an analysis unit depends on.
115    \\  unit_trace [unit]
116    \\    Dump the current reference trace of an analysis unit.
117    \\
118    \\TYPES
119    \\  type_info [id]
120    \\    Dump basic info about a type.
121    \\  type_namespace [id]
122    \\    List all declarations in the namespace of a type.
123    \\
124;
125
126fn handleCommand(zcu: *Zcu, w: *std.Io.Writer, cmd_str: []const u8, arg_str: []const u8) error{ WriteFailed, OutOfMemory }!void {
127    const ip = &zcu.intern_pool;
128    if (std.mem.eql(u8, cmd_str, "help")) {
129        try w.writeAll(help_str);
130    } else if (std.mem.eql(u8, cmd_str, "summary")) {
131        try w.print(
132            \\last generation: {d}
133            \\total container types: {d}
134            \\total NAVs: {d}
135            \\total units: {d}
136            \\
137        , .{
138            zcu.generation - 1,
139            zcu.incremental_debug_state.types.count(),
140            zcu.incremental_debug_state.navs.count(),
141            zcu.incremental_debug_state.units.count(),
142        });
143    } else if (std.mem.eql(u8, cmd_str, "nav_info")) {
144        const nav_index: InternPool.Nav.Index = @enumFromInt(parseIndex(arg_str) orelse return w.writeAll("malformed nav index"));
145        const create_gen = zcu.incremental_debug_state.navs.get(nav_index) orelse return w.writeAll("unknown nav index");
146        const nav = ip.getNav(nav_index);
147        try w.print(
148            \\name: '{f}'
149            \\fqn: '{f}'
150            \\status: {s}
151            \\created on generation: {d}
152            \\
153        , .{
154            nav.name.fmt(ip),
155            nav.fqn.fmt(ip),
156            @tagName(nav.status),
157            create_gen,
158        });
159        switch (nav.status) {
160            .unresolved => {},
161            .type_resolved, .fully_resolved => {
162                try w.writeAll("type: ");
163                try printType(.fromInterned(nav.typeOf(ip)), zcu, w);
164                try w.writeByte('\n');
165            },
166        }
167    } else if (std.mem.eql(u8, cmd_str, "find_type")) {
168        if (arg_str.len == 0) return w.writeAll("bad usage");
169        const anchor_start = arg_str[0] == '^';
170        const anchor_end = arg_str[arg_str.len - 1] == '$';
171        const query = arg_str[@intFromBool(anchor_start) .. arg_str.len - @intFromBool(anchor_end)];
172        var num_results: usize = 0;
173        for (zcu.incremental_debug_state.types.keys()) |type_ip_index| {
174            const ty: Type = .fromInterned(type_ip_index);
175            const ty_name = ty.containerTypeName(ip).toSlice(ip);
176            const success = switch (@as(u2, @intFromBool(anchor_start)) << 1 | @intFromBool(anchor_end)) {
177                0b00 => std.mem.indexOf(u8, ty_name, query) != null,
178                0b01 => std.mem.endsWith(u8, ty_name, query),
179                0b10 => std.mem.startsWith(u8, ty_name, query),
180                0b11 => std.mem.eql(u8, ty_name, query),
181            };
182            if (success) {
183                num_results += 1;
184                try w.print("* type {d} ('{s}')\n", .{ @intFromEnum(type_ip_index), ty_name });
185            }
186        }
187        try w.print("Found {d} results\n", .{num_results});
188    } else if (std.mem.eql(u8, cmd_str, "find_nav")) {
189        if (arg_str.len == 0) return w.writeAll("bad usage");
190        const anchor_start = arg_str[0] == '^';
191        const anchor_end = arg_str[arg_str.len - 1] == '$';
192        const query = arg_str[@intFromBool(anchor_start) .. arg_str.len - @intFromBool(anchor_end)];
193        var num_results: usize = 0;
194        for (zcu.incremental_debug_state.navs.keys()) |nav_index| {
195            const nav = ip.getNav(nav_index);
196            const nav_fqn = nav.fqn.toSlice(ip);
197            const success = switch (@as(u2, @intFromBool(anchor_start)) << 1 | @intFromBool(anchor_end)) {
198                0b00 => std.mem.indexOf(u8, nav_fqn, query) != null,
199                0b01 => std.mem.endsWith(u8, nav_fqn, query),
200                0b10 => std.mem.startsWith(u8, nav_fqn, query),
201                0b11 => std.mem.eql(u8, nav_fqn, query),
202            };
203            if (success) {
204                num_results += 1;
205                try w.print("* nav {d} ('{s}')\n", .{ @intFromEnum(nav_index), nav_fqn });
206            }
207        }
208        try w.print("Found {d} results\n", .{num_results});
209    } else if (std.mem.eql(u8, cmd_str, "unit_info")) {
210        const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
211        const unit_info = zcu.incremental_debug_state.units.get(unit) orelse return w.writeAll("unknown anal unit");
212        var ref_str_buf: [32]u8 = undefined;
213        const ref_str: []const u8 = ref: {
214            const refs = try zcu.resolveReferences();
215            const ref = refs.get(unit) orelse break :ref "<unreferenced>";
216            const referencer = (ref orelse break :ref "<analysis root>").referencer;
217            break :ref printAnalUnit(referencer, &ref_str_buf);
218        };
219        const has_err: []const u8 = err: {
220            if (zcu.failed_analysis.contains(unit)) break :err "true";
221            if (zcu.transitive_failed_analysis.contains(unit)) break :err "true (transitive)";
222            break :err "false";
223        };
224        try w.print(
225            \\last update generation: {d}
226            \\current referencer: {s}
227            \\has error: {s}
228            \\
229        , .{
230            unit_info.last_update_gen,
231            ref_str,
232            has_err,
233        });
234    } else if (std.mem.eql(u8, cmd_str, "unit_dependencies")) {
235        const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
236        const unit_info = zcu.incremental_debug_state.units.get(unit) orelse return w.writeAll("unknown anal unit");
237        for (unit_info.deps.items, 0..) |dependee, i| {
238            try w.print("[{d}] ", .{i});
239            switch (dependee) {
240                .src_hash, .namespace, .namespace_name, .zon_file, .embed_file => try w.print("{f}", .{zcu.fmtDependee(dependee)}),
241                .nav_val, .nav_ty => |nav| try w.print("{s} {d}", .{ @tagName(dependee), @intFromEnum(nav) }),
242                .interned => |ip_index| switch (ip.indexToKey(ip_index)) {
243                    .struct_type, .union_type, .enum_type => try w.print("type {d}", .{@intFromEnum(ip_index)}),
244                    .func => try w.print("func {d}", .{@intFromEnum(ip_index)}),
245                    else => unreachable,
246                },
247                .memoized_state => |stage| try w.print("memoized_state {s}", .{@tagName(stage)}),
248            }
249            try w.writeByte('\n');
250        }
251    } else if (std.mem.eql(u8, cmd_str, "unit_trace")) {
252        const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
253        if (!zcu.incremental_debug_state.units.contains(unit)) return w.writeAll("unknown anal unit");
254        const refs = try zcu.resolveReferences();
255        if (!refs.contains(unit)) return w.writeAll("not referenced");
256        var opt_cur: ?AnalUnit = unit;
257        while (opt_cur) |cur| {
258            var buf: [32]u8 = undefined;
259            try w.print("* {s}\n", .{printAnalUnit(cur, &buf)});
260            opt_cur = if (refs.get(cur).?) |ref| ref.referencer else null;
261        }
262    } else if (std.mem.eql(u8, cmd_str, "type_info")) {
263        const ip_index: InternPool.Index = @enumFromInt(parseIndex(arg_str) orelse return w.writeAll("malformed ip index"));
264        const create_gen = zcu.incremental_debug_state.types.get(ip_index) orelse return w.writeAll("unknown type");
265        try w.print(
266            \\name: '{f}'
267            \\created on generation: {d}
268            \\
269        , .{
270            Type.fromInterned(ip_index).containerTypeName(ip).fmt(ip),
271            create_gen,
272        });
273    } else if (std.mem.eql(u8, cmd_str, "type_namespace")) {
274        const ip_index: InternPool.Index = @enumFromInt(parseIndex(arg_str) orelse return w.writeAll("malformed ip index"));
275        if (!zcu.incremental_debug_state.types.contains(ip_index)) return w.writeAll("unknown type");
276        const ns = zcu.namespacePtr(Type.fromInterned(ip_index).getNamespaceIndex(zcu));
277        try w.print("{d} pub decls:\n", .{ns.pub_decls.count()});
278        for (ns.pub_decls.keys()) |nav| {
279            try w.print("* nav {d}\n", .{@intFromEnum(nav)});
280        }
281        try w.print("{d} non-pub decls:\n", .{ns.priv_decls.count()});
282        for (ns.priv_decls.keys()) |nav| {
283            try w.print("* nav {d}\n", .{@intFromEnum(nav)});
284        }
285        try w.print("{d} comptime decls:\n", .{ns.comptime_decls.items.len});
286        for (ns.comptime_decls.items) |id| {
287            try w.print("* comptime {d}\n", .{@intFromEnum(id)});
288        }
289        try w.print("{d} tests:\n", .{ns.test_decls.items.len});
290        for (ns.test_decls.items) |nav| {
291            try w.print("* nav {d}\n", .{@intFromEnum(nav)});
292        }
293    } else {
294        try w.writeAll("command not found; run 'help' for a command list");
295    }
296}
297
298fn parseIndex(str: []const u8) ?u32 {
299    return std.fmt.parseInt(u32, str, 10) catch null;
300}
301fn parseAnalUnit(str: []const u8) ?AnalUnit {
302    const split_idx = std.mem.indexOfScalar(u8, str, ' ') orelse return null;
303    const kind = str[0..split_idx];
304    const idx_str = str[split_idx + 1 ..];
305    if (std.mem.eql(u8, kind, "comptime")) {
306        return .wrap(.{ .@"comptime" = @enumFromInt(parseIndex(idx_str) orelse return null) });
307    } else if (std.mem.eql(u8, kind, "nav_val")) {
308        return .wrap(.{ .nav_val = @enumFromInt(parseIndex(idx_str) orelse return null) });
309    } else if (std.mem.eql(u8, kind, "nav_ty")) {
310        return .wrap(.{ .nav_ty = @enumFromInt(parseIndex(idx_str) orelse return null) });
311    } else if (std.mem.eql(u8, kind, "type")) {
312        return .wrap(.{ .type = @enumFromInt(parseIndex(idx_str) orelse return null) });
313    } else if (std.mem.eql(u8, kind, "func")) {
314        return .wrap(.{ .func = @enumFromInt(parseIndex(idx_str) orelse return null) });
315    } else if (std.mem.eql(u8, kind, "memoized_state")) {
316        return .wrap(.{ .memoized_state = std.meta.stringToEnum(
317            InternPool.MemoizedStateStage,
318            idx_str,
319        ) orelse return null });
320    } else {
321        return null;
322    }
323}
324fn printAnalUnit(unit: AnalUnit, buf: *[32]u8) []const u8 {
325    const idx: u32 = switch (unit.unwrap()) {
326        .memoized_state => |stage| return std.fmt.bufPrint(buf, "memoized_state {s}", .{@tagName(stage)}) catch unreachable,
327        inline else => |i| @intFromEnum(i),
328    };
329    return std.fmt.bufPrint(buf, "{s} {d}", .{ @tagName(unit.unwrap()), idx }) catch unreachable;
330}
331fn printType(ty: Type, zcu: *const Zcu, w: anytype) !void {
332    const ip = &zcu.intern_pool;
333    switch (ip.indexToKey(ty.toIntern())) {
334        .int_type => |int| try w.print("{c}{d}", .{
335            @as(u8, if (int.signedness == .unsigned) 'u' else 'i'),
336            int.bits,
337        }),
338        .tuple_type => try w.writeAll("(tuple)"),
339        .error_set_type => try w.writeAll("(error set)"),
340        .inferred_error_set_type => try w.writeAll("(inferred error set)"),
341        .func_type => try w.writeAll("(function)"),
342        .anyframe_type => try w.writeAll("(anyframe)"),
343        .vector_type => {
344            try w.print("@Vector({d}, ", .{ty.vectorLen(zcu)});
345            try printType(ty.childType(zcu), zcu, w);
346            try w.writeByte(')');
347        },
348        .array_type => {
349            try w.print("[{d}]", .{ty.arrayLen(zcu)});
350            try printType(ty.childType(zcu), zcu, w);
351        },
352        .opt_type => {
353            try w.writeByte('?');
354            try printType(ty.optionalChild(zcu), zcu, w);
355        },
356        .error_union_type => {
357            try printType(ty.errorUnionSet(zcu), zcu, w);
358            try w.writeByte('!');
359            try printType(ty.errorUnionPayload(zcu), zcu, w);
360        },
361        .ptr_type => {
362            try w.writeAll("*(attrs) ");
363            try printType(ty.childType(zcu), zcu, w);
364        },
365        .simple_type => |simple| try w.writeAll(@tagName(simple)),
366
367        .struct_type,
368        .union_type,
369        .enum_type,
370        .opaque_type,
371        => try w.print("{f}[{d}]", .{ ty.containerTypeName(ip).fmt(ip), @intFromEnum(ty.toIntern()) }),
372
373        else => unreachable,
374    }
375}
376
377const std = @import("std");
378const Io = std.Io;
379const Allocator = std.mem.Allocator;
380
381const Compilation = @import("Compilation.zig");
382const Zcu = @import("Zcu.zig");
383const InternPool = @import("InternPool.zig");
384const Type = @import("Type.zig");
385const AnalUnit = InternPool.AnalUnit;
386
387const IncrementalDebugServer = @This();