aboutsummaryrefslogtreecommitdiff
path: root/src/IncrementalDebugServer.zig
blob: d376ec146ee376caa8c93213628c23fbfd8842d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
//! This is a simple TCP server which exposes a REPL useful for debugging incremental compilation
//! issues. Eventually, this logic should move into `std.zig.Client`/`std.zig.Server` or something
//! similar, but for now, this works. The server is enabled by the '--debug-incremental' CLI flag.
//! The easiest way to interact with the REPL is to use `telnet`:
//! ```
//! telnet "::1" 7623
//! ```
//! 'help' will list available commands. When the debug server is enabled, the compiler tracks a lot
//! of extra state (see `Zcu.IncrementalDebugState`), so note that RSS will be higher than usual.

comptime {
    // This file should only be referenced when debug extensions are enabled.
    std.debug.assert(@import("build_options").enable_debug_extensions and !@import("builtin").single_threaded);
}

zcu: *Zcu,
thread: ?std.Thread,
running: std.atomic.Value(bool),
/// Held by our owner when an update is in-progress, and held by us when responding to a command.
/// So, essentially guards all access to `Compilation`, including `Zcu`.
mutex: std.Thread.Mutex,

pub fn init(zcu: *Zcu) IncrementalDebugServer {
    return .{
        .zcu = zcu,
        .thread = null,
        .running = .init(true),
        .mutex = .{},
    };
}

pub fn deinit(ids: *IncrementalDebugServer) void {
    if (ids.thread) |t| {
        ids.running.store(false, .monotonic);
        t.join();
    }
}

const port = 7623;
pub fn spawn(ids: *IncrementalDebugServer) void {
    std.debug.print("spawning incremental debug server on port {d}\n", .{port});
    ids.thread = std.Thread.spawn(.{ .allocator = ids.zcu.comp.arena }, runThread, .{ids}) catch |err|
        std.process.fatal("failed to spawn incremental debug server: {s}", .{@errorName(err)});
}
fn runThread(ids: *IncrementalDebugServer) void {
    const gpa = ids.zcu.gpa;
    const io = ids.zcu.comp.io;

    var cmd_buf: [1024]u8 = undefined;
    var text_out: std.ArrayList(u8) = .empty;
    defer text_out.deinit(gpa);

    const addr: std.Io.net.IpAddress = .{ .ip6 = .loopback(port) };
    var server = addr.listen(io, .{}) catch @panic("IncrementalDebugServer: failed to listen");
    defer server.deinit(io);
    var stream = server.accept(io) catch @panic("IncrementalDebugServer: failed to accept");
    defer stream.close(io);

    var stream_reader = stream.reader(io, &cmd_buf);
    var stream_writer = stream.writer(io, &.{});

    while (ids.running.load(.monotonic)) {
        stream_writer.interface.writeAll("zig> ") catch @panic("IncrementalDebugServer: failed to write");
        const untrimmed = stream_reader.interface.takeSentinel('\n') catch |err| switch (err) {
            error.EndOfStream => break,
            else => @panic("IncrementalDebugServer: failed to read command"),
        };
        const cmd_and_arg = std.mem.trim(u8, untrimmed, " \t\r\n");
        const cmd: []const u8, const arg: []const u8 = if (std.mem.indexOfScalar(u8, cmd_and_arg, ' ')) |i|
            .{ cmd_and_arg[0..i], cmd_and_arg[i + 1 ..] }
        else
            .{ cmd_and_arg, "" };

        text_out.clearRetainingCapacity();
        {
            if (!ids.mutex.tryLock()) {
                stream_writer.interface.writeAll("waiting for in-progress update to finish...\n") catch @panic("IncrementalDebugServer: failed to write");
                ids.mutex.lock();
            }
            defer ids.mutex.unlock();
            var allocating: std.Io.Writer.Allocating = .fromArrayList(gpa, &text_out);
            defer text_out = allocating.toArrayList();
            handleCommand(ids.zcu, &allocating.writer, cmd, arg) catch @panic("IncrementalDebugServer: out of memory");
        }
        text_out.append(gpa, '\n') catch @panic("IncrementalDebugServer: out of memory");
        stream_writer.interface.writeAll(text_out.items) catch @panic("IncrementalDebugServer: failed to write");
    }
    std.debug.print("closing incremental debug server\n", .{});
}

const help_str: []const u8 =
    \\[str] arguments are any string.
    \\[id] arguments are a numeric ID/index, like an InternPool index.
    \\[unit] arguments are strings like 'func 1234' where '1234' is the relevant index (in this case an InternPool index).
    \\
    \\MISC
    \\  summary
    \\    Dump some information about the whole ZCU.
    \\  nav_info [id]
    \\    Dump basic info about a NAV.
    \\
    \\SEARCHING
    \\  find_type [str]
    \\    Find types (including dead ones) whose names contain the given substring.
    \\    Starting with '^' or ending with '$' anchors to the start/end of the name.
    \\  find_nav [str]
    \\    Find NAVs (including dead ones) whose names contain the given substring.
    \\    Starting with '^' or ending with '$' anchors to the start/end of the name.
    \\
    \\UNITS
    \\  unit_info [unit]
    \\    Dump basic info about an analysis unit.
    \\  unit_dependencies [unit]
    \\    List all units which an analysis unit depends on.
    \\  unit_trace [unit]
    \\    Dump the current reference trace of an analysis unit.
    \\
    \\TYPES
    \\  type_info [id]
    \\    Dump basic info about a type.
    \\  type_namespace [id]
    \\    List all declarations in the namespace of a type.
    \\
;

fn handleCommand(zcu: *Zcu, w: *std.Io.Writer, cmd_str: []const u8, arg_str: []const u8) error{ WriteFailed, OutOfMemory }!void {
    const ip = &zcu.intern_pool;
    if (std.mem.eql(u8, cmd_str, "help")) {
        try w.writeAll(help_str);
    } else if (std.mem.eql(u8, cmd_str, "summary")) {
        try w.print(
            \\last generation: {d}
            \\total container types: {d}
            \\total NAVs: {d}
            \\total units: {d}
            \\
        , .{
            zcu.generation - 1,
            zcu.incremental_debug_state.types.count(),
            zcu.incremental_debug_state.navs.count(),
            zcu.incremental_debug_state.units.count(),
        });
    } else if (std.mem.eql(u8, cmd_str, "nav_info")) {
        const nav_index: InternPool.Nav.Index = @enumFromInt(parseIndex(arg_str) orelse return w.writeAll("malformed nav index"));
        const create_gen = zcu.incremental_debug_state.navs.get(nav_index) orelse return w.writeAll("unknown nav index");
        const nav = ip.getNav(nav_index);
        try w.print(
            \\name: '{f}'
            \\fqn: '{f}'
            \\status: {s}
            \\created on generation: {d}
            \\
        , .{
            nav.name.fmt(ip),
            nav.fqn.fmt(ip),
            @tagName(nav.status),
            create_gen,
        });
        switch (nav.status) {
            .unresolved => {},
            .type_resolved, .fully_resolved => {
                try w.writeAll("type: ");
                try printType(.fromInterned(nav.typeOf(ip)), zcu, w);
                try w.writeByte('\n');
            },
        }
    } else if (std.mem.eql(u8, cmd_str, "find_type")) {
        if (arg_str.len == 0) return w.writeAll("bad usage");
        const anchor_start = arg_str[0] == '^';
        const anchor_end = arg_str[arg_str.len - 1] == '$';
        const query = arg_str[@intFromBool(anchor_start) .. arg_str.len - @intFromBool(anchor_end)];
        var num_results: usize = 0;
        for (zcu.incremental_debug_state.types.keys()) |type_ip_index| {
            const ty: Type = .fromInterned(type_ip_index);
            const ty_name = ty.containerTypeName(ip).toSlice(ip);
            const success = switch (@as(u2, @intFromBool(anchor_start)) << 1 | @intFromBool(anchor_end)) {
                0b00 => std.mem.indexOf(u8, ty_name, query) != null,
                0b01 => std.mem.endsWith(u8, ty_name, query),
                0b10 => std.mem.startsWith(u8, ty_name, query),
                0b11 => std.mem.eql(u8, ty_name, query),
            };
            if (success) {
                num_results += 1;
                try w.print("* type {d} ('{s}')\n", .{ @intFromEnum(type_ip_index), ty_name });
            }
        }
        try w.print("Found {d} results\n", .{num_results});
    } else if (std.mem.eql(u8, cmd_str, "find_nav")) {
        if (arg_str.len == 0) return w.writeAll("bad usage");
        const anchor_start = arg_str[0] == '^';
        const anchor_end = arg_str[arg_str.len - 1] == '$';
        const query = arg_str[@intFromBool(anchor_start) .. arg_str.len - @intFromBool(anchor_end)];
        var num_results: usize = 0;
        for (zcu.incremental_debug_state.navs.keys()) |nav_index| {
            const nav = ip.getNav(nav_index);
            const nav_fqn = nav.fqn.toSlice(ip);
            const success = switch (@as(u2, @intFromBool(anchor_start)) << 1 | @intFromBool(anchor_end)) {
                0b00 => std.mem.indexOf(u8, nav_fqn, query) != null,
                0b01 => std.mem.endsWith(u8, nav_fqn, query),
                0b10 => std.mem.startsWith(u8, nav_fqn, query),
                0b11 => std.mem.eql(u8, nav_fqn, query),
            };
            if (success) {
                num_results += 1;
                try w.print("* nav {d} ('{s}')\n", .{ @intFromEnum(nav_index), nav_fqn });
            }
        }
        try w.print("Found {d} results\n", .{num_results});
    } else if (std.mem.eql(u8, cmd_str, "unit_info")) {
        const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
        const unit_info = zcu.incremental_debug_state.units.get(unit) orelse return w.writeAll("unknown anal unit");
        var ref_str_buf: [32]u8 = undefined;
        const ref_str: []const u8 = ref: {
            const refs = try zcu.resolveReferences();
            const ref = refs.get(unit) orelse break :ref "<unreferenced>";
            const referencer = (ref orelse break :ref "<analysis root>").referencer;
            break :ref printAnalUnit(referencer, &ref_str_buf);
        };
        const has_err: []const u8 = err: {
            if (zcu.failed_analysis.contains(unit)) break :err "true";
            if (zcu.transitive_failed_analysis.contains(unit)) break :err "true (transitive)";
            break :err "false";
        };
        try w.print(
            \\last update generation: {d}
            \\current referencer: {s}
            \\has error: {s}
            \\
        , .{
            unit_info.last_update_gen,
            ref_str,
            has_err,
        });
    } else if (std.mem.eql(u8, cmd_str, "unit_dependencies")) {
        const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
        const unit_info = zcu.incremental_debug_state.units.get(unit) orelse return w.writeAll("unknown anal unit");
        for (unit_info.deps.items, 0..) |dependee, i| {
            try w.print("[{d}] ", .{i});
            switch (dependee) {
                .src_hash, .namespace, .namespace_name, .zon_file, .embed_file => try w.print("{f}", .{zcu.fmtDependee(dependee)}),
                .nav_val, .nav_ty => |nav| try w.print("{s} {d}", .{ @tagName(dependee), @intFromEnum(nav) }),
                .interned => |ip_index| switch (ip.indexToKey(ip_index)) {
                    .struct_type, .union_type, .enum_type => try w.print("type {d}", .{@intFromEnum(ip_index)}),
                    .func => try w.print("func {d}", .{@intFromEnum(ip_index)}),
                    else => unreachable,
                },
                .memoized_state => |stage| try w.print("memoized_state {s}", .{@tagName(stage)}),
            }
            try w.writeByte('\n');
        }
    } else if (std.mem.eql(u8, cmd_str, "unit_trace")) {
        const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
        if (!zcu.incremental_debug_state.units.contains(unit)) return w.writeAll("unknown anal unit");
        const refs = try zcu.resolveReferences();
        if (!refs.contains(unit)) return w.writeAll("not referenced");
        var opt_cur: ?AnalUnit = unit;
        while (opt_cur) |cur| {
            var buf: [32]u8 = undefined;
            try w.print("* {s}\n", .{printAnalUnit(cur, &buf)});
            opt_cur = if (refs.get(cur).?) |ref| ref.referencer else null;
        }
    } else if (std.mem.eql(u8, cmd_str, "type_info")) {
        const ip_index: InternPool.Index = @enumFromInt(parseIndex(arg_str) orelse return w.writeAll("malformed ip index"));
        const create_gen = zcu.incremental_debug_state.types.get(ip_index) orelse return w.writeAll("unknown type");
        try w.print(
            \\name: '{f}'
            \\created on generation: {d}
            \\
        , .{
            Type.fromInterned(ip_index).containerTypeName(ip).fmt(ip),
            create_gen,
        });
    } else if (std.mem.eql(u8, cmd_str, "type_namespace")) {
        const ip_index: InternPool.Index = @enumFromInt(parseIndex(arg_str) orelse return w.writeAll("malformed ip index"));
        if (!zcu.incremental_debug_state.types.contains(ip_index)) return w.writeAll("unknown type");
        const ns = zcu.namespacePtr(Type.fromInterned(ip_index).getNamespaceIndex(zcu));
        try w.print("{d} pub decls:\n", .{ns.pub_decls.count()});
        for (ns.pub_decls.keys()) |nav| {
            try w.print("* nav {d}\n", .{@intFromEnum(nav)});
        }
        try w.print("{d} non-pub decls:\n", .{ns.priv_decls.count()});
        for (ns.priv_decls.keys()) |nav| {
            try w.print("* nav {d}\n", .{@intFromEnum(nav)});
        }
        try w.print("{d} comptime decls:\n", .{ns.comptime_decls.items.len});
        for (ns.comptime_decls.items) |id| {
            try w.print("* comptime {d}\n", .{@intFromEnum(id)});
        }
        try w.print("{d} tests:\n", .{ns.test_decls.items.len});
        for (ns.test_decls.items) |nav| {
            try w.print("* nav {d}\n", .{@intFromEnum(nav)});
        }
    } else {
        try w.writeAll("command not found; run 'help' for a command list");
    }
}

fn parseIndex(str: []const u8) ?u32 {
    return std.fmt.parseInt(u32, str, 10) catch null;
}
fn parseAnalUnit(str: []const u8) ?AnalUnit {
    const split_idx = std.mem.indexOfScalar(u8, str, ' ') orelse return null;
    const kind = str[0..split_idx];
    const idx_str = str[split_idx + 1 ..];
    if (std.mem.eql(u8, kind, "comptime")) {
        return .wrap(.{ .@"comptime" = @enumFromInt(parseIndex(idx_str) orelse return null) });
    } else if (std.mem.eql(u8, kind, "nav_val")) {
        return .wrap(.{ .nav_val = @enumFromInt(parseIndex(idx_str) orelse return null) });
    } else if (std.mem.eql(u8, kind, "nav_ty")) {
        return .wrap(.{ .nav_ty = @enumFromInt(parseIndex(idx_str) orelse return null) });
    } else if (std.mem.eql(u8, kind, "type")) {
        return .wrap(.{ .type = @enumFromInt(parseIndex(idx_str) orelse return null) });
    } else if (std.mem.eql(u8, kind, "func")) {
        return .wrap(.{ .func = @enumFromInt(parseIndex(idx_str) orelse return null) });
    } else if (std.mem.eql(u8, kind, "memoized_state")) {
        return .wrap(.{ .memoized_state = std.meta.stringToEnum(
            InternPool.MemoizedStateStage,
            idx_str,
        ) orelse return null });
    } else {
        return null;
    }
}
fn printAnalUnit(unit: AnalUnit, buf: *[32]u8) []const u8 {
    const idx: u32 = switch (unit.unwrap()) {
        .memoized_state => |stage| return std.fmt.bufPrint(buf, "memoized_state {s}", .{@tagName(stage)}) catch unreachable,
        inline else => |i| @intFromEnum(i),
    };
    return std.fmt.bufPrint(buf, "{s} {d}", .{ @tagName(unit.unwrap()), idx }) catch unreachable;
}
fn printType(ty: Type, zcu: *const Zcu, w: anytype) !void {
    const ip = &zcu.intern_pool;
    switch (ip.indexToKey(ty.toIntern())) {
        .int_type => |int| try w.print("{c}{d}", .{
            @as(u8, if (int.signedness == .unsigned) 'u' else 'i'),
            int.bits,
        }),
        .tuple_type => try w.writeAll("(tuple)"),
        .error_set_type => try w.writeAll("(error set)"),
        .inferred_error_set_type => try w.writeAll("(inferred error set)"),
        .func_type => try w.writeAll("(function)"),
        .anyframe_type => try w.writeAll("(anyframe)"),
        .vector_type => {
            try w.print("@Vector({d}, ", .{ty.vectorLen(zcu)});
            try printType(ty.childType(zcu), zcu, w);
            try w.writeByte(')');
        },
        .array_type => {
            try w.print("[{d}]", .{ty.arrayLen(zcu)});
            try printType(ty.childType(zcu), zcu, w);
        },
        .opt_type => {
            try w.writeByte('?');
            try printType(ty.optionalChild(zcu), zcu, w);
        },
        .error_union_type => {
            try printType(ty.errorUnionSet(zcu), zcu, w);
            try w.writeByte('!');
            try printType(ty.errorUnionPayload(zcu), zcu, w);
        },
        .ptr_type => {
            try w.writeAll("*(attrs) ");
            try printType(ty.childType(zcu), zcu, w);
        },
        .simple_type => |simple| try w.writeAll(@tagName(simple)),

        .struct_type,
        .union_type,
        .enum_type,
        .opaque_type,
        => try w.print("{f}[{d}]", .{ ty.containerTypeName(ip).fmt(ip), @intFromEnum(ty.toIntern()) }),

        else => unreachable,
    }
}

const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;

const Compilation = @import("Compilation.zig");
const Zcu = @import("Zcu.zig");
const InternPool = @import("InternPool.zig");
const Type = @import("Type.zig");
const AnalUnit = InternPool.AnalUnit;

const IncrementalDebugServer = @This();