master
  1const std = @import("std");
  2const Io = std.Io;
  3const Allocator = std.mem.Allocator;
  4const Cache = std.Build.Cache;
  5
  6const usage = "usage: incr-check <zig binary path> <input file> [--zig-lib-dir lib] [--debug-zcu] [--debug-dwarf] [--debug-link] [--preserve-tmp] [--zig-cc-binary /path/to/zig]";
  7
  8pub fn main() !void {
  9    const fatal = std.process.fatal;
 10
 11    var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
 12    defer arena_instance.deinit();
 13    const arena = arena_instance.allocator();
 14
 15    const gpa = arena;
 16
 17    var threaded: Io.Threaded = .init(gpa);
 18    defer threaded.deinit();
 19    const io = threaded.io();
 20
 21    var opt_zig_exe: ?[]const u8 = null;
 22    var opt_input_file_name: ?[]const u8 = null;
 23    var opt_lib_dir: ?[]const u8 = null;
 24    var opt_cc_zig: ?[]const u8 = null;
 25    var debug_zcu = false;
 26    var debug_dwarf = false;
 27    var debug_link = false;
 28    var preserve_tmp = false;
 29
 30    var arg_it = try std.process.argsWithAllocator(arena);
 31    _ = arg_it.skip();
 32    while (arg_it.next()) |arg| {
 33        if (arg.len > 0 and arg[0] == '-') {
 34            if (std.mem.eql(u8, arg, "--zig-lib-dir")) {
 35                opt_lib_dir = arg_it.next() orelse fatal("expected arg after '--zig-lib-dir'\n{s}", .{usage});
 36            } else if (std.mem.eql(u8, arg, "--debug-zcu")) {
 37                debug_zcu = true;
 38            } else if (std.mem.eql(u8, arg, "--debug-dwarf")) {
 39                debug_dwarf = true;
 40            } else if (std.mem.eql(u8, arg, "--debug-link")) {
 41                debug_link = true;
 42            } else if (std.mem.eql(u8, arg, "--preserve-tmp")) {
 43                preserve_tmp = true;
 44            } else if (std.mem.eql(u8, arg, "--zig-cc-binary")) {
 45                opt_cc_zig = arg_it.next() orelse fatal("expect arg after '--zig-cc-binary'\n{s}", .{usage});
 46            } else {
 47                fatal("unknown option '{s}'\n{s}", .{ arg, usage });
 48            }
 49            continue;
 50        }
 51        if (opt_zig_exe == null) {
 52            opt_zig_exe = arg;
 53        } else if (opt_input_file_name == null) {
 54            opt_input_file_name = arg;
 55        } else {
 56            fatal("unknown argument '{s}'\n{s}", .{ arg, usage });
 57        }
 58    }
 59    const zig_exe = opt_zig_exe orelse fatal("missing path to zig\n{s}", .{usage});
 60    const input_file_name = opt_input_file_name orelse fatal("missing input file\n{s}", .{usage});
 61
 62    const input_file_bytes = try std.fs.cwd().readFileAlloc(input_file_name, arena, .limited(std.math.maxInt(u32)));
 63    const case = try Case.parse(arena, io, input_file_bytes);
 64
 65    // Check now: if there are any targets using the `cbe` backend, we need the lib dir.
 66    if (opt_lib_dir == null) {
 67        for (case.targets) |target| {
 68            if (target.backend == .cbe) {
 69                fatal("'--zig-lib-dir' requried when using backend 'cbe'", .{});
 70            }
 71        }
 72    }
 73
 74    const prog_node = std.Progress.start(.{});
 75    defer prog_node.end();
 76
 77    const rand_int = std.crypto.random.int(u64);
 78    const tmp_dir_path = "tmp_" ++ std.fmt.hex(rand_int);
 79    var tmp_dir = try std.fs.cwd().makeOpenPath(tmp_dir_path, .{});
 80    defer {
 81        tmp_dir.close();
 82        if (!preserve_tmp) {
 83            std.fs.cwd().deleteTree(tmp_dir_path) catch |err| {
 84                std.log.warn("failed to delete tree '{s}': {s}", .{ tmp_dir_path, @errorName(err) });
 85            };
 86        }
 87    }
 88
 89    // Convert paths to be relative to the cwd of the subprocess.
 90    const resolved_zig_exe = try std.fs.path.relative(arena, tmp_dir_path, zig_exe);
 91    const opt_resolved_lib_dir = if (opt_lib_dir) |lib_dir|
 92        try std.fs.path.relative(arena, tmp_dir_path, lib_dir)
 93    else
 94        null;
 95
 96    const host = try std.zig.system.resolveTargetQuery(io, .{});
 97
 98    const debug_log_verbose = debug_zcu or debug_dwarf or debug_link;
 99
100    for (case.targets) |target| {
101        const target_prog_node = node: {
102            var name_buf: [std.Progress.Node.max_name_len]u8 = undefined;
103            const name = std.fmt.bufPrint(&name_buf, "{s}-{t}", .{ target.query, target.backend }) catch &name_buf;
104            break :node prog_node.start(name, case.updates.len);
105        };
106        defer target_prog_node.end();
107
108        if (debug_log_verbose) {
109            std.log.scoped(.status).info("target: '{s}-{t}'", .{ target.query, target.backend });
110        }
111        var child_args: std.ArrayList([]const u8) = .empty;
112        try child_args.appendSlice(arena, &.{
113            resolved_zig_exe,
114            "build-exe",
115            "-fincremental",
116            "-fno-ubsan-rt",
117            "-target",
118            target.query,
119            "--cache-dir",
120            ".local-cache",
121            "--global-cache-dir",
122            ".global-cache",
123        });
124        if (target.resolved.os.tag == .windows) try child_args.append(arena, "-lws2_32");
125        try child_args.append(arena, "--listen=-");
126
127        if (opt_resolved_lib_dir) |resolved_lib_dir| {
128            try child_args.appendSlice(arena, &.{ "--zig-lib-dir", resolved_lib_dir });
129        }
130        switch (target.backend) {
131            .sema => try child_args.append(arena, "-fno-emit-bin"),
132            .selfhosted => try child_args.appendSlice(arena, &.{ "-fno-llvm", "-fno-lld" }),
133            .llvm => try child_args.appendSlice(arena, &.{ "-fllvm", "-flld" }),
134            .cbe => try child_args.appendSlice(arena, &.{ "-ofmt=c", "-lc" }),
135        }
136        if (debug_zcu) {
137            try child_args.appendSlice(arena, &.{ "--debug-log", "zcu" });
138        }
139        if (debug_dwarf) {
140            try child_args.appendSlice(arena, &.{ "--debug-log", "dwarf" });
141        }
142        if (debug_link) {
143            try child_args.appendSlice(arena, &.{ "--debug-log", "link", "--debug-log", "link_state", "--debug-log", "link_relocs" });
144        }
145        for (case.modules) |mod| {
146            try child_args.appendSlice(arena, &.{ "--dep", mod.name });
147        }
148        try child_args.append(arena, try std.fmt.allocPrint(arena, "-Mroot={s}", .{case.root_source_file}));
149        for (case.modules) |mod| {
150            try child_args.append(arena, try std.fmt.allocPrint(arena, "-M{s}={s}", .{ mod.name, mod.file }));
151        }
152
153        const zig_prog_node = target_prog_node.start("zig build-exe", 0);
154        defer zig_prog_node.end();
155
156        var child = std.process.Child.init(child_args.items, arena);
157        child.stdin_behavior = .Pipe;
158        child.stdout_behavior = .Pipe;
159        child.stderr_behavior = .Pipe;
160        child.progress_node = zig_prog_node;
161        child.cwd_dir = tmp_dir;
162        child.cwd = tmp_dir_path;
163
164        var cc_child_args: std.ArrayList([]const u8) = .empty;
165        if (target.backend == .cbe) {
166            const resolved_cc_zig_exe = if (opt_cc_zig) |cc_zig_exe|
167                try std.fs.path.relative(arena, tmp_dir_path, cc_zig_exe)
168            else
169                resolved_zig_exe;
170
171            try cc_child_args.appendSlice(arena, &.{
172                resolved_cc_zig_exe,
173                "cc",
174                "-target",
175                target.query,
176                "-I",
177                opt_resolved_lib_dir.?, // verified earlier
178            });
179
180            if (target.resolved.os.tag == .windows)
181                try cc_child_args.append(arena, "-lws2_32");
182
183            try cc_child_args.append(arena, "-o");
184        }
185
186        var eval: Eval = .{
187            .arena = arena,
188            .case = case,
189            .host = host,
190            .target = target,
191            .tmp_dir = tmp_dir,
192            .tmp_dir_path = tmp_dir_path,
193            .child = &child,
194            .allow_stderr = debug_log_verbose,
195            .preserve_tmp_on_fatal = preserve_tmp,
196            .cc_child_args = &cc_child_args,
197        };
198
199        try child.spawn();
200        errdefer {
201            _ = child.kill() catch {};
202        }
203
204        var poller = Io.poll(arena, Eval.StreamEnum, .{
205            .stdout = child.stdout.?,
206            .stderr = child.stderr.?,
207        });
208        defer poller.deinit();
209
210        for (case.updates) |update| {
211            var update_node = target_prog_node.start(update.name, 0);
212            defer update_node.end();
213
214            if (debug_log_verbose) {
215                std.log.scoped(.status).info("update: '{s}'", .{update.name});
216            }
217
218            eval.write(update);
219            try eval.requestUpdate();
220            try eval.check(&poller, update, update_node);
221        }
222
223        try eval.end(&poller);
224
225        waitChild(&child, &eval);
226    }
227}
228
229const Eval = struct {
230    arena: Allocator,
231    host: std.Target,
232    case: Case,
233    target: Case.Target,
234    tmp_dir: std.fs.Dir,
235    tmp_dir_path: []const u8,
236    child: *std.process.Child,
237    allow_stderr: bool,
238    preserve_tmp_on_fatal: bool,
239    /// When `target.backend == .cbe`, this contains the first few arguments to `zig cc` to build the generated binary.
240    /// The arguments `out.c in.c` must be appended before spawning the subprocess.
241    cc_child_args: *std.ArrayList([]const u8),
242
243    const StreamEnum = enum { stdout, stderr };
244    const Poller = Io.Poller(StreamEnum);
245
246    /// Currently this function assumes the previous updates have already been written.
247    fn write(eval: *Eval, update: Case.Update) void {
248        for (update.changes) |full_contents| {
249            eval.tmp_dir.writeFile(.{
250                .sub_path = full_contents.name,
251                .data = full_contents.bytes,
252            }) catch |err| {
253                eval.fatal("failed to update '{s}': {s}", .{ full_contents.name, @errorName(err) });
254            };
255        }
256        for (update.deletes) |doomed_name| {
257            eval.tmp_dir.deleteFile(doomed_name) catch |err| {
258                eval.fatal("failed to delete '{s}': {s}", .{ doomed_name, @errorName(err) });
259            };
260        }
261    }
262
263    fn check(eval: *Eval, poller: *Poller, update: Case.Update, prog_node: std.Progress.Node) !void {
264        const arena = eval.arena;
265        const stdout = poller.reader(.stdout);
266        const stderr = poller.reader(.stderr);
267
268        poll: while (true) {
269            const Header = std.zig.Server.Message.Header;
270            while (stdout.buffered().len < @sizeOf(Header)) if (!try poller.poll()) break :poll;
271            const header = stdout.takeStruct(Header, .little) catch unreachable;
272            while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
273            const body = stdout.take(header.bytes_len) catch unreachable;
274
275            switch (header.tag) {
276                .error_bundle => {
277                    const result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
278                    if (stderr.bufferedLen() > 0) {
279                        const stderr_data = try poller.toOwnedSlice(.stderr);
280                        if (eval.allow_stderr) {
281                            std.log.info("error_bundle included stderr:\n{s}", .{stderr_data});
282                        } else {
283                            eval.fatal("error_bundle included unexpected stderr:\n{s}", .{stderr_data});
284                        }
285                    }
286                    if (result_error_bundle.errorMessageCount() != 0) {
287                        try eval.checkErrorOutcome(update, result_error_bundle);
288                    }
289                    // This message indicates the end of the update.
290                    return;
291                },
292                .emit_digest => {
293                    var r: std.Io.Reader = .fixed(body);
294                    _ = r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch unreachable;
295                    if (stderr.bufferedLen() > 0) {
296                        const stderr_data = try poller.toOwnedSlice(.stderr);
297                        if (eval.allow_stderr) {
298                            std.log.info("emit_digest included stderr:\n{s}", .{stderr_data});
299                        } else {
300                            eval.fatal("emit_digest included unexpected stderr:\n{s}", .{stderr_data});
301                        }
302                    }
303
304                    if (eval.target.backend == .sema) {
305                        try eval.checkSuccessOutcome(update, null, prog_node);
306                        // This message indicates the end of the update.
307                    }
308
309                    const digest = r.takeArray(Cache.bin_digest_len) catch unreachable;
310                    const result_dir = ".local-cache" ++ std.fs.path.sep_str ++ "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*);
311
312                    const bin_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{
313                        .root_name = "root", // corresponds to the module name "root"
314                        .target = &eval.target.resolved,
315                        .output_mode = .Exe,
316                    });
317                    const bin_path = try std.fs.path.join(arena, &.{ result_dir, bin_name });
318
319                    try eval.checkSuccessOutcome(update, bin_path, prog_node);
320                    // This message indicates the end of the update.
321                },
322                else => {
323                    // Ignore other messages.
324                },
325            }
326        }
327
328        if (stderr.bufferedLen() > 0) {
329            if (eval.allow_stderr) {
330                std.log.info("update '{s}' included stderr:\n{s}", .{ update.name, stderr.buffered() });
331            } else {
332                eval.fatal("update '{s}' failed:\n{s}", .{ update.name, stderr.buffered() });
333            }
334        }
335
336        waitChild(eval.child, eval);
337        eval.fatal("update '{s}': compiler failed to send error_bundle or emit_bin_path", .{update.name});
338    }
339
340    fn checkErrorOutcome(eval: *Eval, update: Case.Update, error_bundle: std.zig.ErrorBundle) !void {
341        const expected = switch (update.outcome) {
342            .unknown => return,
343            .compile_errors => |ce| ce,
344            .stdout, .exit_code => {
345                error_bundle.renderToStdErr(.{}, .auto);
346                eval.fatal("update '{s}': unexpected compile errors", .{update.name});
347            },
348        };
349
350        var expected_idx: usize = 0;
351
352        for (error_bundle.getMessages()) |err_idx| {
353            if (expected_idx == expected.errors.len) {
354                error_bundle.renderToStdErr(.{}, .auto);
355                eval.fatal("update '{s}': more errors than expected", .{update.name});
356            }
357            try eval.checkOneError(update, error_bundle, expected.errors[expected_idx], false, err_idx);
358            expected_idx += 1;
359
360            for (error_bundle.getNotes(err_idx)) |note_idx| {
361                if (expected_idx == expected.errors.len) {
362                    error_bundle.renderToStdErr(.{}, .auto);
363                    eval.fatal("update '{s}': more error notes than expected", .{update.name});
364                }
365                try eval.checkOneError(update, error_bundle, expected.errors[expected_idx], true, note_idx);
366                expected_idx += 1;
367            }
368        }
369
370        if (!std.mem.eql(u8, error_bundle.getCompileLogOutput(), expected.compile_log_output)) {
371            error_bundle.renderToStdErr(.{}, .auto);
372            eval.fatal("update '{s}': unexpected compile log output", .{update.name});
373        }
374    }
375
376    fn checkOneError(
377        eval: *Eval,
378        update: Case.Update,
379        eb: std.zig.ErrorBundle,
380        expected: Case.ExpectedError,
381        is_note: bool,
382        err_idx: std.zig.ErrorBundle.MessageIndex,
383    ) Allocator.Error!void {
384        const err = eb.getErrorMessage(err_idx);
385        if (err.src_loc == .none) @panic("TODO error message with no source location");
386        if (err.count != 1) @panic("TODO error message with count>1");
387        const msg = eb.nullTerminatedString(err.msg);
388        const src = eb.getSourceLocation(err.src_loc);
389        const raw_filename = eb.nullTerminatedString(src.src_path);
390
391        // We need to replace backslashes for consistency between platforms.
392        const filename = name: {
393            if (std.mem.indexOfScalar(u8, raw_filename, '\\') == null) break :name raw_filename;
394            const copied = try eval.arena.dupe(u8, raw_filename);
395            std.mem.replaceScalar(u8, copied, '\\', '/');
396            break :name copied;
397        };
398
399        if (expected.is_note != is_note or
400            !std.mem.eql(u8, expected.filename, filename) or
401            expected.line != src.line + 1 or
402            expected.column != src.column + 1 or
403            !std.mem.eql(u8, expected.msg, msg))
404        {
405            eb.renderToStdErr(.{}, .auto);
406            eval.fatal("update '{s}': compile error did not match expected error", .{update.name});
407        }
408    }
409
410    fn checkSuccessOutcome(eval: *Eval, update: Case.Update, opt_emitted_path: ?[]const u8, prog_node: std.Progress.Node) !void {
411        switch (update.outcome) {
412            .unknown => return,
413            .compile_errors => eval.fatal("expected compile errors but compilation incorrectly succeeded", .{}),
414            .stdout, .exit_code => {},
415        }
416        const emitted_path = opt_emitted_path orelse {
417            std.debug.assert(eval.target.backend == .sema);
418            return;
419        };
420
421        const binary_path = switch (eval.target.backend) {
422            .sema => unreachable,
423            .selfhosted, .llvm => emitted_path,
424            .cbe => bin: {
425                const rand_int = std.crypto.random.int(u64);
426                const out_bin_name = "./out_" ++ std.fmt.hex(rand_int);
427                try eval.buildCOutput(update, emitted_path, out_bin_name, prog_node);
428                break :bin out_bin_name;
429            },
430        };
431
432        var argv_buf: [2][]const u8 = undefined;
433        const argv: []const []const u8, const is_foreign: bool = switch (std.zig.system.getExternalExecutor(
434            &eval.host,
435            &eval.target.resolved,
436            .{ .link_libc = eval.target.backend == .cbe },
437        )) {
438            .bad_dl, .bad_os_or_cpu => {
439                // This binary cannot be executed on this host.
440                if (eval.allow_stderr) {
441                    std.log.warn("skipping execution because host '{s}' cannot execute binaries for foreign target '{s}'", .{
442                        try eval.host.zigTriple(eval.arena),
443                        try eval.target.resolved.zigTriple(eval.arena),
444                    });
445                }
446                return;
447            },
448            .native, .rosetta => argv: {
449                argv_buf[0] = binary_path;
450                break :argv .{ argv_buf[0..1], false };
451            },
452            .qemu, .wine, .wasmtime, .darling => |executor_cmd| argv: {
453                argv_buf[0] = executor_cmd;
454                argv_buf[1] = binary_path;
455                break :argv .{ argv_buf[0..2], true };
456            },
457        };
458
459        const run_prog_node = prog_node.start("run generated executable", 0);
460        defer run_prog_node.end();
461
462        const result = std.process.Child.run(.{
463            .allocator = eval.arena,
464            .argv = argv,
465            .cwd_dir = eval.tmp_dir,
466            .cwd = eval.tmp_dir_path,
467        }) catch |err| {
468            if (is_foreign) {
469                // Chances are the foreign executor isn't available. Skip this evaluation.
470                if (eval.allow_stderr) {
471                    std.log.warn("update '{s}': skipping execution of '{s}' via executor for foreign target '{s}': {s}", .{
472                        update.name,
473                        binary_path,
474                        try eval.target.resolved.zigTriple(eval.arena),
475                        @errorName(err),
476                    });
477                }
478                return;
479            }
480            eval.fatal("update '{s}': failed to run the generated executable '{s}': {s}", .{
481                update.name, binary_path, @errorName(err),
482            });
483        };
484
485        // Some executors (looking at you, Wine) like throwing some stderr in, just for fun.
486        // Therefore, we'll ignore stderr when using a foreign executor.
487        if (!is_foreign and result.stderr.len != 0) {
488            std.log.err("update '{s}': generated executable '{s}' had unexpected stderr:\n{s}", .{
489                update.name, binary_path, result.stderr,
490            });
491        }
492
493        switch (result.term) {
494            .Exited => |code| switch (update.outcome) {
495                .unknown, .compile_errors => unreachable,
496                .stdout => |expected_stdout| {
497                    if (code != 0) {
498                        eval.fatal("update '{s}': generated executable '{s}' failed with code {d}", .{
499                            update.name, binary_path, code,
500                        });
501                    }
502                    try std.testing.expectEqualStrings(expected_stdout, result.stdout);
503                },
504                .exit_code => |expected_code| try std.testing.expectEqual(expected_code, result.term.Exited),
505            },
506            .Signal, .Stopped, .Unknown => {
507                eval.fatal("update '{s}': generated executable '{s}' terminated unexpectedly", .{
508                    update.name, binary_path,
509                });
510            },
511        }
512
513        if (!is_foreign and result.stderr.len != 0) std.process.exit(1);
514    }
515
516    fn requestUpdate(eval: *Eval) !void {
517        const header: std.zig.Client.Message.Header = .{
518            .tag = .update,
519            .bytes_len = 0,
520        };
521        var w = eval.child.stdin.?.writer(&.{});
522        w.interface.writeStruct(header, .little) catch |err| switch (err) {
523            error.WriteFailed => return w.err.?,
524        };
525    }
526
527    fn end(eval: *Eval, poller: *Poller) !void {
528        requestExit(eval.child, eval);
529
530        const stdout = poller.reader(.stdout);
531        const stderr = poller.reader(.stderr);
532
533        poll: while (true) {
534            const Header = std.zig.Server.Message.Header;
535            while (stdout.buffered().len < @sizeOf(Header)) if (!try poller.poll()) break :poll;
536            const header = stdout.takeStruct(Header, .little) catch unreachable;
537            while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
538            stdout.toss(header.bytes_len);
539        }
540
541        if (stderr.bufferedLen() > 0) {
542            eval.fatal("unexpected stderr:\n{s}", .{stderr.buffered()});
543        }
544    }
545
546    fn buildCOutput(eval: *Eval, update: Case.Update, c_path: []const u8, out_path: []const u8, prog_node: std.Progress.Node) !void {
547        std.debug.assert(eval.cc_child_args.items.len > 0);
548
549        const child_prog_node = prog_node.start("build cbe output", 0);
550        defer child_prog_node.end();
551
552        try eval.cc_child_args.appendSlice(eval.arena, &.{ out_path, c_path });
553        defer eval.cc_child_args.items.len -= 2;
554
555        const result = std.process.Child.run(.{
556            .allocator = eval.arena,
557            .argv = eval.cc_child_args.items,
558            .cwd_dir = eval.tmp_dir,
559            .cwd = eval.tmp_dir_path,
560            .progress_node = child_prog_node,
561        }) catch |err| {
562            eval.fatal("update '{s}': failed to spawn zig cc for '{s}': {s}", .{
563                update.name, c_path, @errorName(err),
564            });
565        };
566        switch (result.term) {
567            .Exited => |code| if (code != 0) {
568                if (result.stderr.len != 0) {
569                    std.log.err("update '{s}': zig cc stderr:\n{s}", .{
570                        update.name, result.stderr,
571                    });
572                }
573                eval.fatal("update '{s}': zig cc for '{s}' failed with code {d}", .{
574                    update.name, c_path, code,
575                });
576            },
577            .Signal, .Stopped, .Unknown => {
578                if (result.stderr.len != 0) {
579                    std.log.err("update '{s}': zig cc stderr:\n{s}", .{
580                        update.name, result.stderr,
581                    });
582                }
583                eval.fatal("update '{s}': zig cc for '{s}' terminated unexpectedly", .{
584                    update.name, c_path,
585                });
586            },
587        }
588    }
589
590    fn fatal(eval: *Eval, comptime fmt: []const u8, args: anytype) noreturn {
591        eval.tmp_dir.close();
592        if (!eval.preserve_tmp_on_fatal) {
593            // Kill the child since it holds an open handle to its CWD which is the tmp dir path
594            _ = eval.child.kill() catch {};
595            std.fs.cwd().deleteTree(eval.tmp_dir_path) catch |err| {
596                std.log.warn("failed to delete tree '{s}': {s}", .{ eval.tmp_dir_path, @errorName(err) });
597            };
598        }
599        std.process.fatal(fmt, args);
600    }
601};
602
603const Case = struct {
604    updates: []Update,
605    root_source_file: []const u8,
606    targets: []const Target,
607    modules: []const Module,
608
609    const Target = struct {
610        query: []const u8,
611        resolved: std.Target,
612        backend: Backend,
613        const Backend = enum {
614            /// Run semantic analysis only. Runtime output will not be tested, but we still verify
615            /// that compilation succeeds. Corresponds to `-fno-emit-bin`.
616            sema,
617            /// Use the self-hosted code generation backend for this target.
618            /// Corresponds to `-fno-llvm -fno-lld`.
619            selfhosted,
620            /// Use the LLVM backend.
621            /// Corresponds to `-fllvm -flld`.
622            llvm,
623            /// Use the C backend. The output is compiled with `zig cc`.
624            /// Corresponds to `-ofmt=c`.
625            cbe,
626        };
627    };
628
629    const Module = struct {
630        name: []const u8,
631        file: []const u8,
632    };
633
634    const Update = struct {
635        name: []const u8,
636        outcome: Outcome,
637        changes: []const FullContents = &.{},
638        deletes: []const []const u8 = &.{},
639    };
640
641    const FullContents = struct {
642        name: []const u8,
643        bytes: []const u8,
644    };
645
646    const Outcome = union(enum) {
647        unknown,
648        compile_errors: struct {
649            errors: []const ExpectedError,
650            compile_log_output: []const u8,
651        },
652        stdout: []const u8,
653        exit_code: u8,
654    };
655
656    const ExpectedError = struct {
657        is_note: bool,
658        filename: []const u8,
659        line: u32,
660        column: u32,
661        msg: []const u8,
662    };
663
664    fn parse(arena: Allocator, io: Io, bytes: []const u8) !Case {
665        const fatal = std.process.fatal;
666
667        var targets: std.ArrayList(Target) = .empty;
668        var modules: std.ArrayList(Module) = .empty;
669        var updates: std.ArrayList(Update) = .empty;
670        var changes: std.ArrayList(FullContents) = .empty;
671        var deletes: std.ArrayList([]const u8) = .empty;
672        var it = std.mem.splitScalar(u8, bytes, '\n');
673        var line_n: usize = 1;
674        var root_source_file: ?[]const u8 = null;
675        while (it.next()) |line| : (line_n += 1) {
676            if (std.mem.startsWith(u8, line, "#")) {
677                var line_it = std.mem.splitScalar(u8, line, '=');
678                const key = line_it.first()[1..];
679                const val = std.mem.trimEnd(u8, line_it.rest(), "\r"); // windows moment
680                if (val.len == 0) {
681                    fatal("line {d}: missing value", .{line_n});
682                } else if (std.mem.eql(u8, key, "target")) {
683                    const split_idx = std.mem.lastIndexOfScalar(u8, val, '-') orelse
684                        fatal("line {d}: target does not include backend", .{line_n});
685
686                    const query = val[0..split_idx];
687
688                    const backend_str = val[split_idx + 1 ..];
689                    const backend: Target.Backend = std.meta.stringToEnum(Target.Backend, backend_str) orelse
690                        fatal("line {d}: invalid backend '{s}'", .{ line_n, backend_str });
691
692                    const parsed_query = std.Build.parseTargetQuery(.{
693                        .arch_os_abi = query,
694                        .object_format = switch (backend) {
695                            .sema, .selfhosted, .llvm => null,
696                            .cbe => "c",
697                        },
698                    }) catch fatal("line {d}: invalid target query '{s}'", .{ line_n, query });
699
700                    const resolved = try std.zig.system.resolveTargetQuery(io, parsed_query);
701
702                    try targets.append(arena, .{
703                        .query = query,
704                        .resolved = resolved,
705                        .backend = backend,
706                    });
707                } else if (std.mem.eql(u8, key, "module")) {
708                    const split_idx = std.mem.indexOfScalar(u8, val, '=') orelse
709                        fatal("line {d}: module does not include file", .{line_n});
710                    const name = val[0..split_idx];
711                    const file = val[split_idx + 1 ..];
712                    try modules.append(arena, .{
713                        .name = name,
714                        .file = file,
715                    });
716                } else if (std.mem.eql(u8, key, "update")) {
717                    if (updates.items.len > 0) {
718                        const last_update = &updates.items[updates.items.len - 1];
719                        last_update.changes = try changes.toOwnedSlice(arena);
720                        last_update.deletes = try deletes.toOwnedSlice(arena);
721                    }
722                    try updates.append(arena, .{
723                        .name = val,
724                        .outcome = .unknown,
725                    });
726                } else if (std.mem.eql(u8, key, "file")) {
727                    if (updates.items.len == 0) fatal("line {d}: file directive before update", .{line_n});
728
729                    if (root_source_file == null)
730                        root_source_file = val;
731
732                    // Because Windows is so excellent, we need to convert CRLF to LF, so
733                    // can't just slice into the input here. How delightful!
734                    var src: std.ArrayList(u8) = .empty;
735
736                    while (true) {
737                        const next_line_raw = it.peek() orelse fatal("line {d}: unexpected EOF", .{line_n});
738                        const next_line = std.mem.trimEnd(u8, next_line_raw, "\r");
739                        if (std.mem.startsWith(u8, next_line, "#")) break;
740
741                        _ = it.next();
742                        line_n += 1;
743
744                        try src.ensureUnusedCapacity(arena, next_line.len + 1);
745                        src.appendSliceAssumeCapacity(next_line);
746                        src.appendAssumeCapacity('\n');
747                    }
748
749                    try changes.append(arena, .{
750                        .name = val,
751                        .bytes = src.items,
752                    });
753                } else if (std.mem.eql(u8, key, "rm_file")) {
754                    if (updates.items.len == 0) fatal("line {d}: rm_file directive before update", .{line_n});
755                    try deletes.append(arena, val);
756                } else if (std.mem.eql(u8, key, "expect_stdout")) {
757                    if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n});
758                    const last_update = &updates.items[updates.items.len - 1];
759                    if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n});
760                    last_update.outcome = .{
761                        .stdout = std.zig.string_literal.parseAlloc(arena, val) catch |err| {
762                            fatal("line {d}: bad string literal: {s}", .{ line_n, @errorName(err) });
763                        },
764                    };
765                } else if (std.mem.eql(u8, key, "expect_error")) {
766                    if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n});
767                    const last_update = &updates.items[updates.items.len - 1];
768                    if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n});
769
770                    var errors: std.ArrayList(ExpectedError) = .empty;
771                    try errors.append(arena, parseExpectedError(val, line_n));
772                    while (true) {
773                        const next_line = it.peek() orelse break;
774                        if (!std.mem.startsWith(u8, next_line, "#")) break;
775                        var new_line_it = std.mem.splitScalar(u8, next_line, '=');
776                        const new_key = new_line_it.first()[1..];
777                        const new_val = std.mem.trimEnd(u8, new_line_it.rest(), "\r");
778                        if (new_val.len == 0) break;
779                        if (!std.mem.eql(u8, new_key, "expect_error")) break;
780
781                        _ = it.next();
782                        line_n += 1;
783                        try errors.append(arena, parseExpectedError(new_val, line_n));
784                    }
785
786                    var compile_log_output: std.ArrayList(u8) = .empty;
787                    while (true) {
788                        const next_line = it.peek() orelse break;
789                        if (!std.mem.startsWith(u8, next_line, "#")) break;
790                        var new_line_it = std.mem.splitScalar(u8, next_line, '=');
791                        const new_key = new_line_it.first()[1..];
792                        const new_val = std.mem.trimEnd(u8, new_line_it.rest(), "\r");
793                        if (new_val.len == 0) break;
794                        if (!std.mem.eql(u8, new_key, "expect_compile_log")) break;
795
796                        _ = it.next();
797                        line_n += 1;
798                        try compile_log_output.ensureUnusedCapacity(arena, new_val.len + 1);
799                        compile_log_output.appendSliceAssumeCapacity(new_val);
800                        compile_log_output.appendAssumeCapacity('\n');
801                    }
802
803                    last_update.outcome = .{ .compile_errors = .{
804                        .errors = errors.items,
805                        .compile_log_output = compile_log_output.items,
806                    } };
807                } else if (std.mem.eql(u8, key, "expect_compile_log")) {
808                    fatal("line {d}: 'expect_compile_log' must immediately follow 'expect_error'", .{line_n});
809                } else {
810                    fatal("line {d}: unrecognized key '{s}'", .{ line_n, key });
811                }
812            }
813        }
814
815        if (targets.items.len == 0) {
816            fatal("missing target", .{});
817        }
818
819        if (changes.items.len > 0) {
820            const last_update = &updates.items[updates.items.len - 1];
821            last_update.changes = changes.items; // arena so no need for toOwnedSlice
822            last_update.deletes = deletes.items;
823        }
824
825        return .{
826            .updates = updates.items,
827            .root_source_file = root_source_file orelse fatal("missing root source file", .{}),
828            .targets = targets.items, // arena so no need for toOwnedSlice
829            .modules = modules.items,
830        };
831    }
832};
833
834fn requestExit(child: *std.process.Child, eval: *Eval) void {
835    if (child.stdin == null) return;
836
837    const header: std.zig.Client.Message.Header = .{
838        .tag = .exit,
839        .bytes_len = 0,
840    };
841    var w = eval.child.stdin.?.writer(&.{});
842    w.interface.writeStruct(header, .little) catch |err| switch (err) {
843        error.WriteFailed => switch (w.err.?) {
844            error.BrokenPipe => {},
845            else => |e| eval.fatal("failed to send exit: {s}", .{@errorName(e)}),
846        },
847    };
848
849    // Send EOF to stdin.
850    child.stdin.?.close();
851    child.stdin = null;
852}
853
854fn waitChild(child: *std.process.Child, eval: *Eval) void {
855    requestExit(child, eval);
856    const term = child.wait() catch |err| eval.fatal("child process failed: {s}", .{@errorName(err)});
857    switch (term) {
858        .Exited => |code| if (code != 0) eval.fatal("compiler failed with code {d}", .{code}),
859        .Signal, .Stopped, .Unknown => eval.fatal("compiler terminated unexpectedly", .{}),
860    }
861}
862
863fn parseExpectedError(str: []const u8, l: usize) Case.ExpectedError {
864    // #expect_error=foo.zig:1:2: error: the error message
865    // #expect_error=foo.zig:1:2: note: and a note
866
867    const fatal = std.process.fatal;
868
869    var it = std.mem.splitScalar(u8, str, ':');
870    const filename = it.first();
871    const line_str = it.next() orelse fatal("line {d}: incomplete error specification", .{l});
872    const column_str = it.next() orelse fatal("line {d}: incomplete error specification", .{l});
873    const error_or_note_str = std.mem.trim(
874        u8,
875        it.next() orelse fatal("line {d}: incomplete error specification", .{l}),
876        " ",
877    );
878    const message = std.mem.trim(u8, it.rest(), " ");
879    if (filename.len == 0) fatal("line {d}: empty filename", .{l});
880    if (message.len == 0) fatal("line {d}: empty error message", .{l});
881    const is_note = if (std.mem.eql(u8, error_or_note_str, "error"))
882        false
883    else if (std.mem.eql(u8, error_or_note_str, "note"))
884        true
885    else
886        fatal("line {d}: expeted 'error' or 'note', found '{s}'", .{ l, error_or_note_str });
887
888    const line = std.fmt.parseInt(u32, line_str, 10) catch
889        fatal("line {d}: invalid line number '{s}'", .{ l, line_str });
890
891    const column = std.fmt.parseInt(u32, column_str, 10) catch
892        fatal("line {d}: invalid column number '{s}'", .{ l, column_str });
893
894    return .{
895        .is_note = is_note,
896        .filename = filename,
897        .line = line,
898        .column = column,
899        .msg = message,
900    };
901}