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,
17future: ?Io.Future(void),
18/// Held by our owner when an update is in-progress, and held by us when responding to a command.
19/// So, essentially guards all access to `Compilation`, including `Zcu`.
20mutex: std.Io.Mutex,
21
22pub fn init(zcu: *Zcu) IncrementalDebugServer {
23 return .{
24 .zcu = zcu,
25 .future = null,
26 .mutex = .init,
27 };
28}
29
30pub fn deinit(ids: *IncrementalDebugServer) void {
31 const io = ids.zcu.comp.io;
32 if (ids.future) |*f| f.cancel(io);
33}
34
35const port = 7623;
36pub fn spawn(ids: *IncrementalDebugServer) void {
37 const io = ids.zcu.comp.io;
38 std.debug.print("spawning incremental debug server on port {d}\n", .{port});
39 ids.future = io.concurrent(runServer, .{ids}) catch |err|
40 std.process.fatal("failed to start incremental debug server: {s}", .{@errorName(err)});
41}
42fn runServer(ids: *IncrementalDebugServer) void {
43 const io = ids.zcu.comp.io;
44
45 const addr: Io.net.IpAddress = .{ .ip6 = .loopback(port) };
46 var server = addr.listen(io, .{}) catch |err| switch (err) {
47 error.Canceled => return,
48 else => |e| {
49 log.err("listen failed ({t}); closing server", .{e});
50 return;
51 },
52 };
53 defer server.deinit(io);
54
55 while (true) {
56 var stream = server.accept(io) catch |err| switch (err) {
57 error.Canceled => return,
58 error.ConnectionAborted => {
59 log.warn("client disconnected during accept", .{});
60 continue;
61 },
62 else => |e| {
63 log.err("accept failed ({t})", .{e});
64 return;
65 },
66 };
67 defer stream.close(io);
68 log.info("client '{f}' connected", .{stream.socket.address});
69 var cmd_buf: [1024]u8 = undefined;
70 var reader = stream.reader(io, &cmd_buf);
71 var writer = stream.writer(io, &.{});
72 ids.serveStream(&reader.interface, &writer.interface) catch |orig_err| {
73 const actual_err = switch (orig_err) {
74 error.Canceled,
75 error.OutOfMemory,
76 error.EndOfStream,
77 error.StreamTooLong,
78 => |e| e,
79
80 error.ReadFailed => reader.err.?,
81 error.WriteFailed => writer.err.?,
82 };
83 switch (actual_err) {
84 error.Canceled => return,
85
86 error.OutOfMemory,
87 error.Unexpected,
88 error.SystemResources,
89 error.NetworkDown,
90 error.NetworkUnreachable,
91 error.HostUnreachable,
92 error.FastOpenAlreadyInProgress,
93 error.ConnectionRefused,
94 error.StreamTooLong,
95 => |e| log.err("failed to serve '{f}' ({t})", .{ stream.socket.address, e }),
96
97 error.EndOfStream,
98 error.ConnectionTimedOut,
99 error.ConnectionResetByPeer,
100 => log.info("client '{f}' disconnected", .{stream.socket.address}),
101
102 error.AddressFamilyUnsupported,
103 error.SocketUnconnected,
104 error.SocketNotBound,
105 error.AccessDenied,
106 => unreachable,
107 }
108 };
109 }
110}
111
112fn serveStream(
113 ids: *IncrementalDebugServer,
114 stream_reader: *Io.Reader,
115 stream_writer: *Io.Writer,
116) error{
117 Canceled,
118 OutOfMemory,
119 EndOfStream,
120 StreamTooLong,
121 ReadFailed,
122 WriteFailed,
123}!noreturn {
124 const gpa = ids.zcu.gpa;
125 const io = ids.zcu.comp.io;
126
127 var text_out: std.ArrayList(u8) = .empty;
128 defer text_out.deinit(gpa);
129
130 while (true) {
131 try stream_writer.writeAll("zig> ");
132 const untrimmed = try stream_reader.takeSentinel('\n');
133 const cmd_and_arg = std.mem.trim(u8, untrimmed, " \t\r\n");
134 const cmd: []const u8, const arg: []const u8 = if (std.mem.findScalar(u8, cmd_and_arg, ' ')) |i|
135 .{ cmd_and_arg[0..i], cmd_and_arg[i + 1 ..] }
136 else
137 .{ cmd_and_arg, "" };
138
139 text_out.clearRetainingCapacity();
140 {
141 if (!ids.mutex.tryLock()) {
142 try stream_writer.writeAll("waiting for in-progress update to finish...\n");
143 try ids.mutex.lock(io);
144 }
145 defer ids.mutex.unlock(io);
146 var allocating: Io.Writer.Allocating = .fromArrayList(gpa, &text_out);
147 defer text_out = allocating.toArrayList();
148 handleCommand(ids.zcu, &allocating.writer, cmd, arg) catch |err| switch (err) {
149 error.OutOfMemory,
150 error.WriteFailed,
151 => return error.OutOfMemory,
152 };
153 }
154 try text_out.append(gpa, '\n');
155 try stream_writer.writeAll(text_out.items);
156 }
157}
158
159const help_str: []const u8 =
160 \\[str] arguments are any string.
161 \\[id] arguments are a numeric ID/index, like an InternPool index.
162 \\[unit] arguments are strings like 'func 1234' where '1234' is the relevant index (in this case an InternPool index).
163 \\
164 \\MISC
165 \\ summary
166 \\ Dump some information about the whole ZCU.
167 \\ nav_info [id]
168 \\ Dump basic info about a NAV.
169 \\
170 \\SEARCHING
171 \\ find_type [str]
172 \\ Find types (including dead ones) whose names contain the given substring.
173 \\ Starting with '^' or ending with '$' anchors to the start/end of the name.
174 \\ find_nav [str]
175 \\ Find NAVs (including dead ones) whose names contain the given substring.
176 \\ Starting with '^' or ending with '$' anchors to the start/end of the name.
177 \\
178 \\UNITS
179 \\ unit_info [unit]
180 \\ Dump basic info about an analysis unit.
181 \\ unit_dependencies [unit]
182 \\ List all units which an analysis unit depends on.
183 \\ unit_trace [unit]
184 \\ Dump the current reference trace of an analysis unit.
185 \\
186 \\TYPES
187 \\ type_info [id]
188 \\ Dump basic info about a type.
189 \\ type_namespace [id]
190 \\ List all declarations in the namespace of a type.
191 \\
192;
193
194fn handleCommand(zcu: *Zcu, w: *Io.Writer, cmd_str: []const u8, arg_str: []const u8) error{ WriteFailed, OutOfMemory }!void {
195 const ip = &zcu.intern_pool;
196 if (std.mem.eql(u8, cmd_str, "help")) {
197 try w.writeAll(help_str);
198 } else if (std.mem.eql(u8, cmd_str, "summary")) {
199 try w.print(
200 \\last generation: {d}
201 \\total container types: {d}
202 \\total NAVs: {d}
203 \\total units: {d}
204 \\
205 , .{
206 zcu.generation - 1,
207 zcu.incremental_debug_state.types.count(),
208 zcu.incremental_debug_state.navs.count(),
209 zcu.incremental_debug_state.units.count(),
210 });
211 } else if (std.mem.eql(u8, cmd_str, "nav_info")) {
212 const nav_index: InternPool.Nav.Index = @fromBackingInt(@intCast(parseIndex(arg_str) orelse return w.writeAll("malformed nav index")));
213 const create_gen = zcu.incremental_debug_state.navs.get(nav_index) orelse return w.writeAll("unknown nav index");
214 const nav = ip.getNav(nav_index);
215 try w.print(
216 \\name: '{f}'
217 \\fqn: '{f}'
218 \\created on generation: {d}
219 \\
220 , .{
221 nav.name.fmt(ip),
222 nav.fqn.fmt(ip),
223 create_gen,
224 });
225 if (nav.resolved) |r| {
226 try w.writeAll("status: resolved\n type: ");
227 try printType(.fromInterned(r.type), zcu, w);
228 try w.writeAll("\n value: ");
229 if (r.value == .none) {
230 try w.writeAll("(unresolved)");
231 } else {
232 try printType(.fromInterned(r.type), zcu, w);
233 }
234 try w.writeByte('\n');
235 } else {
236 try w.writeAll("status: unresolved\n");
237 }
238 } else if (std.mem.eql(u8, cmd_str, "find_type")) {
239 if (arg_str.len == 0) return w.writeAll("bad usage");
240 const anchor_start = arg_str[0] == '^';
241 const anchor_end = arg_str[arg_str.len - 1] == '$';
242 const query = arg_str[@intFromBool(anchor_start) .. arg_str.len - @intFromBool(anchor_end)];
243 var num_results: usize = 0;
244 for (zcu.incremental_debug_state.types.keys()) |type_ip_index| {
245 const ty: Type = .fromInterned(type_ip_index);
246 const ty_name = ty.containerTypeName(ip).fqn.toSlice(ip);
247 const success = switch (@as(u2, @intFromBool(anchor_start)) << 1 | @intFromBool(anchor_end)) {
248 0b00 => std.mem.find(u8, ty_name, query) != null,
249 0b01 => std.mem.endsWith(u8, ty_name, query),
250 0b10 => std.mem.startsWith(u8, ty_name, query),
251 0b11 => std.mem.eql(u8, ty_name, query),
252 };
253 if (success) {
254 num_results += 1;
255 try w.print("* type {d} ('{s}')\n", .{ @backingInt(type_ip_index), ty_name });
256 }
257 }
258 try w.print("Found {d} results\n", .{num_results});
259 } else if (std.mem.eql(u8, cmd_str, "find_nav")) {
260 if (arg_str.len == 0) return w.writeAll("bad usage");
261 const anchor_start = arg_str[0] == '^';
262 const anchor_end = arg_str[arg_str.len - 1] == '$';
263 const query = arg_str[@intFromBool(anchor_start) .. arg_str.len - @intFromBool(anchor_end)];
264 var num_results: usize = 0;
265 for (zcu.incremental_debug_state.navs.keys()) |nav_index| {
266 const nav = ip.getNav(nav_index);
267 const nav_fqn = nav.fqn.toSlice(ip);
268 const success = switch (@as(u2, @intFromBool(anchor_start)) << 1 | @intFromBool(anchor_end)) {
269 0b00 => std.mem.find(u8, nav_fqn, query) != null,
270 0b01 => std.mem.endsWith(u8, nav_fqn, query),
271 0b10 => std.mem.startsWith(u8, nav_fqn, query),
272 0b11 => std.mem.eql(u8, nav_fqn, query),
273 };
274 if (success) {
275 num_results += 1;
276 try w.print("* nav {d} ('{s}')\n", .{ @backingInt(nav_index), nav_fqn });
277 }
278 }
279 try w.print("Found {d} results\n", .{num_results});
280 } else if (std.mem.eql(u8, cmd_str, "unit_info")) {
281 const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
282 const unit_info = zcu.incremental_debug_state.units.get(unit) orelse return w.writeAll("unknown anal unit");
283 var ref_str_buf: [32]u8 = undefined;
284 const ref_str: []const u8 = ref: {
285 const refs = try zcu.resolveReferences();
286 const ref = refs.get(unit) orelse break :ref "<unreferenced>";
287 const referencer = (ref orelse break :ref "<analysis root>").referencer;
288 break :ref printAnalUnit(referencer, &ref_str_buf);
289 };
290 try w.print(
291 \\last update generation: {d}
292 \\current referencer: {s}
293 \\
294 , .{
295 unit_info.last_update_gen,
296 ref_str,
297 });
298 if (zcu.failed_analysis.get(unit)) |err_msg| {
299 try w.print("analysis result: failure ({q})\n", .{err_msg.msg});
300 } else if (zcu.transitive_failed_analysis.get(unit)) |reason| {
301 switch (reason) {
302 .astgen_error => try w.writeAll("analysis result: transitive failure (astgen error)\n"),
303 .dependency_loop => try w.writeAll("analysis result: transitive failure (dependency loop)\n"),
304 .lost_tracking => try w.writeAll("analysis result: transitive failure (lost tracking for zir inst)\n"),
305 .failed_unit => |other_unit| {
306 var buf: [32]u8 = undefined;
307 try w.print("analysis result: transitive failure (failed unit: {s})\n", .{printAnalUnit(other_unit, &buf)});
308 },
309 .func_nav_val_changed => |func_index| try w.print("analysis result: transitive failure (owner nav of func '{d}' changed value)\n", .{@backingInt(func_index)}),
310 }
311 } else {
312 try w.writeAll("analysis result: success\n");
313 }
314 if (unit.unwrap() == .func) {
315 const nav_id = zcu.intern_pool.indexToKey(unit.unwrap().func).func.owner_nav;
316 try w.print("owner nav: {d}\n", .{@backingInt(nav_id)});
317 }
318 } else if (std.mem.eql(u8, cmd_str, "unit_dependencies")) {
319 const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
320 const unit_info = zcu.incremental_debug_state.units.get(unit) orelse return w.writeAll("unknown anal unit");
321 for (unit_info.deps.items, 0..) |dependee, i| {
322 try w.print("[{d}] ", .{i});
323 switch (dependee) {
324 .src_hash, .namespace, .namespace_name, .source_file, .embed_file => try w.print("{f}", .{zcu.fmtDependee(dependee)}),
325 .nav_val, .nav_ty => |nav| try w.print("{t} {d}", .{ dependee, @backingInt(nav) }),
326 .type_layout, .struct_defaults, .func_ies => |ip_index| try w.print("{t} {d}", .{ dependee, @backingInt(ip_index) }),
327 .memoized_state => |stage| try w.print("memoized_state {s}", .{@tagName(stage)}),
328 }
329 try w.writeByte('\n');
330 }
331 } else if (std.mem.eql(u8, cmd_str, "unit_trace")) {
332 const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit");
333 if (!zcu.incremental_debug_state.units.contains(unit)) return w.writeAll("unknown anal unit");
334 const refs = try zcu.resolveReferences();
335 if (!refs.contains(unit)) return w.writeAll("not referenced");
336 var opt_cur: ?AnalUnit = unit;
337 while (opt_cur) |cur| {
338 var buf: [32]u8 = undefined;
339 try w.print("* {s}\n", .{printAnalUnit(cur, &buf)});
340 opt_cur = if (refs.get(cur).?) |ref| ref.referencer else null;
341 }
342 } else if (std.mem.eql(u8, cmd_str, "type_info")) {
343 const ip_index: InternPool.Index = @fromBackingInt(@intCast(parseIndex(arg_str) orelse return w.writeAll("malformed ip index")));
344 const create_gen = zcu.incremental_debug_state.types.get(ip_index) orelse return w.writeAll("unknown type");
345 try w.print(
346 \\name: '{f}'
347 \\created on generation: {d}
348 \\
349 , .{
350 Type.fromInterned(ip_index).containerTypeName(ip).fqn.fmt(ip),
351 create_gen,
352 });
353 } else if (std.mem.eql(u8, cmd_str, "type_namespace")) {
354 const ip_index: InternPool.Index = @fromBackingInt(@intCast(parseIndex(arg_str) orelse return w.writeAll("malformed ip index")));
355 if (!zcu.incremental_debug_state.types.contains(ip_index)) return w.writeAll("unknown type");
356 const ns = zcu.namespacePtr(Type.fromInterned(ip_index).getNamespaceIndex(zcu));
357 try w.print("{d} pub decls:\n", .{ns.pub_decls.count()});
358 for (ns.pub_decls.keys()) |nav| {
359 try w.print("* nav {d}\n", .{@backingInt(nav)});
360 }
361 try w.print("{d} non-pub decls:\n", .{ns.priv_decls.count()});
362 for (ns.priv_decls.keys()) |nav| {
363 try w.print("* nav {d}\n", .{@backingInt(nav)});
364 }
365 try w.print("{d} comptime decls:\n", .{ns.comptime_decls.items.len});
366 for (ns.comptime_decls.items) |id| {
367 try w.print("* comptime {d}\n", .{@backingInt(id)});
368 }
369 try w.print("{d} tests:\n", .{ns.test_decls.items.len});
370 for (ns.test_decls.items) |nav| {
371 try w.print("* nav {d}\n", .{@backingInt(nav)});
372 }
373 } else {
374 try w.writeAll("command not found; run 'help' for a command list");
375 }
376}
377
378fn parseIndex(str: []const u8) ?u32 {
379 return std.fmt.parseInt(u32, str, 10) catch null;
380}
381fn parseAnalUnit(str: []const u8) ?AnalUnit {
382 const split_idx = std.mem.findScalar(u8, str, ' ') orelse return null;
383 const kind = str[0..split_idx];
384 const idx_str = str[split_idx + 1 ..];
385 if (std.mem.eql(u8, kind, "comptime")) {
386 return .wrap(.{ .@"comptime" = @fromBackingInt(@intCast(parseIndex(idx_str) orelse return null)) });
387 } else if (std.mem.eql(u8, kind, "nav_val")) {
388 return .wrap(.{ .nav_val = @fromBackingInt(@intCast(parseIndex(idx_str) orelse return null)) });
389 } else if (std.mem.eql(u8, kind, "nav_ty")) {
390 return .wrap(.{ .nav_ty = @fromBackingInt(@intCast(parseIndex(idx_str) orelse return null)) });
391 } else if (std.mem.eql(u8, kind, "type_layout")) {
392 return .wrap(.{ .type_layout = @fromBackingInt(@intCast(parseIndex(idx_str) orelse return null)) });
393 } else if (std.mem.eql(u8, kind, "struct_defaults")) {
394 return .wrap(.{ .struct_defaults = @fromBackingInt(@intCast(parseIndex(idx_str) orelse return null)) });
395 } else if (std.mem.eql(u8, kind, "func")) {
396 return .wrap(.{ .func = @fromBackingInt(@intCast(parseIndex(idx_str) orelse return null)) });
397 } else if (std.mem.eql(u8, kind, "memoized_state")) {
398 return .wrap(.{ .memoized_state = std.meta.stringToEnum(
399 InternPool.MemoizedStateStage,
400 idx_str,
401 ) orelse return null });
402 } else {
403 return null;
404 }
405}
406fn printAnalUnit(unit: AnalUnit, buf: *[32]u8) []const u8 {
407 const idx: u32 = switch (unit.unwrap()) {
408 .memoized_state => |stage| return std.mem.print(buf, "memoized_state {s}", .{@tagName(stage)}) catch unreachable,
409 inline else => |i| @backingInt(i),
410 };
411 return std.mem.print(buf, "{s} {d}", .{ @tagName(unit.unwrap()), idx }) catch unreachable;
412}
413
414fn printType(ty: Type, zcu: *const Zcu, w: *Io.Writer) Io.Writer.Error!void {
415 const ip = &zcu.intern_pool;
416 switch (ip.indexToKey(ty.toIntern())) {
417 .int_type => |int| try w.print("{c}{d}", .{
418 @as(u8, if (int.signedness == .unsigned) 'u' else 'i'),
419 int.bits,
420 }),
421 .tuple_type => try w.writeAll("(tuple)"),
422 .error_set_type => try w.writeAll("(error set)"),
423 .inferred_error_set_type => try w.writeAll("(inferred error set)"),
424 .func_type => try w.writeAll("(function)"),
425 .anyframe_type => try w.writeAll("(anyframe)"),
426 .vector_type => {
427 try w.print("@Vector({d}, ", .{ty.vectorLen(zcu)});
428 try printType(ty.childType(zcu), zcu, w);
429 try w.writeByte(')');
430 },
431 .array_type => {
432 try w.print("[{d}]", .{ty.arrayLen(zcu)});
433 try printType(ty.childType(zcu), zcu, w);
434 },
435 .opt_type => {
436 try w.writeByte('?');
437 try printType(ty.optionalChild(zcu), zcu, w);
438 },
439 .error_union_type => {
440 try printType(ty.errorUnionSet(zcu), zcu, w);
441 try w.writeByte('!');
442 try printType(ty.errorUnionPayload(zcu), zcu, w);
443 },
444 .ptr_type => {
445 try w.writeAll("*(attrs) ");
446 try printType(ty.childType(zcu), zcu, w);
447 },
448 .simple_type => |simple| try w.writeAll(@tagName(simple)),
449
450 .struct_type,
451 .union_type,
452 .enum_type,
453 .opaque_type,
454 => try w.print("{f}[{d}]", .{ ty.containerTypeName(ip).fqn.fmt(ip), @backingInt(ty.toIntern()) }),
455
456 else => unreachable,
457 }
458}
459
460const std = @import("std");
461const Io = std.Io;
462const Allocator = std.mem.Allocator;
463const log = std.log.scoped(.incremental_debug_server);
464
465const Compilation = @import("Compilation.zig");
466const Zcu = @import("Zcu.zig");
467const InternPool = @import("InternPool.zig");
468const Type = @import("Type.zig");
469const AnalUnit = InternPool.AnalUnit;
470
471const IncrementalDebugServer = @This();