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();