Commit 20b4a2cf2c

Andrew Kelley <andrew@ziglang.org>
2020-06-25 03:28:42
self-hosted: add compare output test for new AST->ZIR code
1 parent 5aa3f56
Changed files (5)
lib
src-self-hosted
test
lib/std/zig.zig
@@ -43,6 +43,27 @@ pub fn findLineColumn(source: []const u8, byte_offset: usize) struct { line: usi
     return .{ .line = line, .column = column };
 }
 
+/// Returns the standard file system basename of a binary generated by the Zig compiler.
+pub fn binNameAlloc(
+    allocator: *std.mem.Allocator,
+    root_name: []const u8,
+    target: std.Target,
+    output_mode: std.builtin.OutputMode,
+    link_mode: ?std.builtin.LinkMode,
+) error{OutOfMemory}![]u8 {
+    switch (output_mode) {
+        .Exe => return std.fmt.allocPrint(allocator, "{}{}", .{ root_name, target.exeFileExt() }),
+        .Lib => {
+            const suffix = switch (link_mode orelse .Static) {
+                .Static => target.staticLibSuffix(),
+                .Dynamic => target.dynamicLibSuffix(),
+            };
+            return std.fmt.allocPrint(allocator, "{}{}{}", .{ target.libPrefix(), root_name, suffix });
+        },
+        .Obj => return std.fmt.allocPrint(allocator, "{}{}", .{ root_name, target.oFileExt() }),
+    }
+}
+
 test "" {
     @import("std").meta.refAllDecls(@This());
 }
src-self-hosted/main.zig
@@ -50,8 +50,7 @@ pub fn log(
     const scope_prefix = "(" ++ switch (scope) {
         // Uncomment to hide logs
         //.compiler,
-        .link,
-        => return,
+        .link => return,
 
         else => @tagName(scope),
     } ++ "): ";
@@ -431,21 +430,7 @@ fn buildOutputType(
             std.debug.warn("-fno-emit-bin not supported yet", .{});
             process.exit(1);
         },
-        .yes_default_path => switch (output_mode) {
-            .Exe => try std.fmt.allocPrint(arena, "{}{}", .{ root_name, target_info.target.exeFileExt() }),
-            .Lib => blk: {
-                const suffix = switch (link_mode orelse .Static) {
-                    .Static => target_info.target.staticLibSuffix(),
-                    .Dynamic => target_info.target.dynamicLibSuffix(),
-                };
-                break :blk try std.fmt.allocPrint(arena, "{}{}{}", .{
-                    target_info.target.libPrefix(),
-                    root_name,
-                    suffix,
-                });
-            },
-            .Obj => try std.fmt.allocPrint(arena, "{}{}", .{ root_name, target_info.target.oFileExt() }),
-        },
+        .yes_default_path => try std.zig.binNameAlloc(arena, root_name, target_info.target, output_mode, link_mode),
         .yes => |p| p,
     };
 
src-self-hosted/test.zig
@@ -21,32 +21,7 @@ const ErrorMsg = struct {
 };
 
 pub const TestContext = struct {
-    // TODO: remove these. They are deprecated.
-    zir_cmp_output_cases: std.ArrayList(ZIRCompareOutputCase),
-
-    /// TODO: find a way to treat cases as individual tests (shouldn't show "1 test passed" if there are 200 cases)
-    zir_cases: std.ArrayList(ZIRCase),
-
-    // TODO: remove
-    pub const ZIRCompareOutputCase = struct {
-        name: []const u8,
-        src_list: []const []const u8,
-        expected_stdout_list: []const []const u8,
-    };
-
-    pub const ZIRUpdateType = enum {
-        /// A transformation update transforms the input ZIR and tests against
-        /// the expected output
-        Transformation,
-        /// An error update attempts to compile bad code, and ensures that it
-        /// fails to compile, and for the expected reasons
-        Error,
-        /// An execution update compiles and runs the input ZIR, feeding in
-        /// provided input and ensuring that the outputs match what is expected
-        Execution,
-        /// A compilation update checks that the ZIR compiles without any issues
-        Compiles,
-    };
+    zir_cases: std.ArrayList(Case),
 
     pub const ZIRUpdate = struct {
         /// The input to the current update. We simulate an incremental update
@@ -57,58 +32,55 @@ pub const TestContext = struct {
         /// you can keep it mostly consistent, with small changes, testing the
         /// effects of the incremental compilation.
         src: [:0]const u8,
-        case: union(ZIRUpdateType) {
-            /// The expected output ZIR
+        case: union(enum) {
+            /// A transformation update transforms the input ZIR and tests against
+            /// the expected output ZIR.
             Transformation: [:0]const u8,
+            /// An error update attempts to compile bad code, and ensures that it
+            /// fails to compile, and for the expected reasons.
             /// A slice containing the expected errors *in sequential order*.
             Error: []const ErrorMsg,
-
-            /// Input to feed to the program, and expected outputs.
-            ///
-            /// If stdout, stderr, and exit_code are all null, addZIRCase will
-            /// discard the test. To test for successful compilation, use a
-            /// dedicated Compile update instead.
-            Execution: struct {
-                stdin: ?[]const u8,
-                stdout: ?[]const u8,
-                stderr: ?[]const u8,
-                exit_code: ?u8,
-            },
-            /// A Compiles test checks only that compilation of the given ZIR
-            /// succeeds. To test outputs, use an Execution test. It is good to
-            /// use a Compiles test before an Execution, as the overhead should
-            /// be low (due to incremental compilation) and TODO: provide a way
-            /// to check changed / new / etc decls in testing mode
-            /// (usingnamespace a debug info struct with a comptime flag?)
-            Compiles: void,
+            /// An execution update compiles and runs the input ZIR, feeding in
+            /// provided input and ensuring that the stdout match what is expected.
+            Execution: []const u8,
         },
     };
 
-    /// A ZIRCase consists of a set of *updates*. A update can transform ZIR,
+    /// A Case consists of a set of *updates*. A update can transform ZIR,
     /// compile it, ensure that compilation fails, and more. The same Module is
     /// used for each update, so each update's source is treated as a single file
     /// being updated by the test harness and incrementally compiled.
-    pub const ZIRCase = struct {
+    pub const Case = struct {
         name: []const u8,
         /// The platform the ZIR targets. For non-native platforms, an emulator
         /// such as QEMU is required for tests to complete.
         target: std.zig.CrossTarget,
         updates: std.ArrayList(ZIRUpdate),
+        output_mode: std.builtin.OutputMode,
+        /// Either ".zir" or ".zig"
+        extension: [4]u8,
 
         /// Adds a subcase in which the module is updated with new ZIR, and the
         /// resulting ZIR is validated.
-        pub fn addTransform(self: *ZIRCase, src: [:0]const u8, result: [:0]const u8) void {
+        pub fn addTransform(self: *Case, src: [:0]const u8, result: [:0]const u8) void {
             self.updates.append(.{
                 .src = src,
                 .case = .{ .Transformation = result },
             }) catch unreachable;
         }
 
+        pub fn addCompareOutput(self: *Case, src: [:0]const u8, result: []const u8) void {
+            self.updates.append(.{
+                .src = src,
+                .case = .{ .Execution = result },
+            }) catch unreachable;
+        }
+
         /// Adds a subcase in which the module is updated with invalid ZIR, and
         /// ensures that compilation fails for the expected reasons.
         ///
         /// Errors must be specified in sequential order.
-        pub fn addError(self: *ZIRCase, src: [:0]const u8, errors: []const []const u8) void {
+        pub fn addError(self: *Case, src: [:0]const u8, errors: []const []const u8) void {
             var array = self.updates.allocator.alloc(ErrorMsg, errors.len) catch unreachable;
             for (errors) |e, i| {
                 if (e[0] != ':') {
@@ -146,15 +118,65 @@ pub const TestContext = struct {
         }
     };
 
-    pub fn addZIRMulti(
+    pub fn addExeZIR(
+        ctx: *TestContext,
+        name: []const u8,
+        target: std.zig.CrossTarget,
+    ) *Case {
+        const case = Case{
+            .name = name,
+            .target = target,
+            .updates = std.ArrayList(ZIRUpdate).init(ctx.zir_cases.allocator),
+            .output_mode = .Exe,
+            .extension = ".zir".*,
+        };
+        ctx.zir_cases.append(case) catch unreachable;
+        return &ctx.zir_cases.items[ctx.zir_cases.items.len - 1];
+    }
+
+    pub fn addObjZIR(
+        ctx: *TestContext,
+        name: []const u8,
+        target: std.zig.CrossTarget,
+    ) *Case {
+        const case = Case{
+            .name = name,
+            .target = target,
+            .updates = std.ArrayList(ZIRUpdate).init(ctx.zir_cases.allocator),
+            .output_mode = .Obj,
+            .extension = ".zir".*,
+        };
+        ctx.zir_cases.append(case) catch unreachable;
+        return &ctx.zir_cases.items[ctx.zir_cases.items.len - 1];
+    }
+
+    pub fn addExe(
         ctx: *TestContext,
         name: []const u8,
         target: std.zig.CrossTarget,
-    ) *ZIRCase {
-        const case = ZIRCase{
+    ) *Case {
+        const case = Case{
             .name = name,
             .target = target,
             .updates = std.ArrayList(ZIRUpdate).init(ctx.zir_cases.allocator),
+            .output_mode = .Exe,
+            .extension = ".zig".*,
+        };
+        ctx.zir_cases.append(case) catch unreachable;
+        return &ctx.zir_cases.items[ctx.zir_cases.items.len - 1];
+    }
+
+    pub fn addObj(
+        ctx: *TestContext,
+        name: []const u8,
+        target: std.zig.CrossTarget,
+    ) *Case {
+        const case = Case{
+            .name = name,
+            .target = target,
+            .updates = std.ArrayList(ZIRUpdate).init(ctx.zir_cases.allocator),
+            .output_mode = .Obj,
+            .extension = ".zig".*,
         };
         ctx.zir_cases.append(case) catch unreachable;
         return &ctx.zir_cases.items[ctx.zir_cases.items.len - 1];
@@ -163,14 +185,21 @@ pub const TestContext = struct {
     pub fn addZIRCompareOutput(
         ctx: *TestContext,
         name: []const u8,
-        src_list: []const []const u8,
-        expected_stdout_list: []const []const u8,
+        src: [:0]const u8,
+        expected_stdout: []const u8,
     ) void {
-        ctx.zir_cmp_output_cases.append(.{
-            .name = name,
-            .src_list = src_list,
-            .expected_stdout_list = expected_stdout_list,
-        }) catch unreachable;
+        var c = ctx.addExeZIR(name, .{});
+        c.addCompareOutput(src, expected_stdout);
+    }
+
+    pub fn addCompareOutput(
+        ctx: *TestContext,
+        name: []const u8,
+        src: [:0]const u8,
+        expected_stdout: []const u8,
+    ) void {
+        var c = ctx.addExe(name, .{});
+        c.addCompareOutput(src, expected_stdout);
     }
 
     pub fn addZIRTransform(
@@ -180,7 +209,7 @@ pub const TestContext = struct {
         src: [:0]const u8,
         result: [:0]const u8,
     ) void {
-        var c = ctx.addZIRMulti(name, target);
+        var c = ctx.addObjZIR(name, target);
         c.addTransform(src, result);
     }
 
@@ -191,20 +220,18 @@ pub const TestContext = struct {
         src: [:0]const u8,
         expected_errors: []const []const u8,
     ) void {
-        var c = ctx.addZIRMulti(name, target);
+        var c = ctx.addObjZIR(name, target);
         c.addError(src, expected_errors);
     }
 
     fn init() TestContext {
         const allocator = std.heap.page_allocator;
         return .{
-            .zir_cmp_output_cases = std.ArrayList(ZIRCompareOutputCase).init(allocator),
-            .zir_cases = std.ArrayList(ZIRCase).init(allocator),
+            .zir_cases = std.ArrayList(Case).init(allocator),
         };
     }
 
     fn deinit(self: *TestContext) void {
-        self.zir_cmp_output_cases.deinit();
         for (self.zir_cases.items) |c| {
             for (c.updates.items) |u| {
                 if (u.case == .Error) {
@@ -235,34 +262,24 @@ pub const TestContext = struct {
             progress.refresh();
 
             const info = try std.zig.system.NativeTargetInfo.detect(std.testing.allocator, case.target);
-            try self.runOneZIRCase(std.testing.allocator, &prg_node, case, info.target);
-            try std.testing.allocator_instance.validate();
-        }
-
-        // TODO: wipe the rest of this function
-        for (self.zir_cmp_output_cases.items) |case| {
-            std.testing.base_allocator_instance.reset();
-
-            var prg_node = root_node.start(case.name, case.src_list.len);
-            prg_node.activate();
-            defer prg_node.end();
-
-            // So that we can see which test case failed when the leak checker goes off.
-            progress.refresh();
-
-            try self.runOneZIRCmpOutputCase(std.testing.allocator, &prg_node, case, native_info.target);
+            try self.runOneCase(std.testing.allocator, &prg_node, case, info.target);
             try std.testing.allocator_instance.validate();
         }
     }
 
-    fn runOneZIRCase(self: *TestContext, allocator: *Allocator, prg_node: *std.Progress.Node, case: ZIRCase, target: std.Target) !void {
+    fn runOneCase(self: *TestContext, allocator: *Allocator, prg_node: *std.Progress.Node, case: Case, target: std.Target) !void {
         var tmp = std.testing.tmpDir(.{});
         defer tmp.cleanup();
 
-        const tmp_src_path = "test_case.zir";
+        const root_name = "test_case";
+        const tmp_src_path = try std.fmt.allocPrint(allocator, "{}{}", .{ root_name, case.extension });
+        defer allocator.free(tmp_src_path);
         const root_pkg = try Package.create(allocator, tmp.dir, ".", tmp_src_path);
         defer root_pkg.destroy();
 
+        const bin_name = try std.zig.binNameAlloc(allocator, root_name, target, case.output_mode, null);
+        defer allocator.free(bin_name);
+
         var module = try Module.init(allocator, .{
             .target = target,
             // This is an Executable, as opposed to e.g. a *library*. This does
@@ -271,17 +288,17 @@ pub const TestContext = struct {
             // TODO: support tests for object file building, and library builds
             // and linking. This will require a rework to support multi-file
             // tests.
-            .output_mode = .Obj,
+            .output_mode = case.output_mode,
             // TODO: support testing optimizations
             .optimize_mode = .Debug,
             .bin_file_dir = tmp.dir,
-            .bin_file_path = "test_case.o",
+            .bin_file_path = bin_name,
             .root_pkg = root_pkg,
             .keep_source_files_loaded = true,
         });
         defer module.deinit();
 
-        for (case.updates.items) |update| {
+        for (case.updates.items) |update, update_index| {
             var update_node = prg_node.start("update", 4);
             update_node.activate();
             defer update_node.end();
@@ -293,6 +310,7 @@ pub const TestContext = struct {
 
             var module_node = update_node.start("parse/analysis/codegen", null);
             module_node.activate();
+            try module.makeBinFileWritable();
             try module.update();
             module_node.end();
 
@@ -341,78 +359,41 @@ pub const TestContext = struct {
                         }
                     }
                 },
-
-                else => return error.Unimplemented,
-            }
-        }
-    }
-
-    fn runOneZIRCmpOutputCase(
-        self: *TestContext,
-        allocator: *Allocator,
-        prg_node: *std.Progress.Node,
-        case: ZIRCompareOutputCase,
-        target: std.Target,
-    ) !void {
-        var tmp = std.testing.tmpDir(.{});
-        defer tmp.cleanup();
-
-        const tmp_src_path = "test-case.zir";
-        const root_pkg = try Package.create(allocator, tmp.dir, ".", tmp_src_path);
-        defer root_pkg.destroy();
-
-        var module = try Module.init(allocator, .{
-            .target = target,
-            .output_mode = .Exe,
-            .optimize_mode = .Debug,
-            .bin_file_dir = tmp.dir,
-            .bin_file_path = "a.out",
-            .root_pkg = root_pkg,
-        });
-        defer module.deinit();
-
-        for (case.src_list) |source, i| {
-            var src_node = prg_node.start("update", 2);
-            src_node.activate();
-            defer src_node.end();
-
-            try tmp.dir.writeFile(tmp_src_path, source);
-
-            var update_node = src_node.start("parse,analysis,codegen", null);
-            update_node.activate();
-            try module.makeBinFileWritable();
-            try module.update();
-            update_node.end();
-
-            var exec_result = x: {
-                var exec_node = src_node.start("execute", null);
-                exec_node.activate();
-                defer exec_node.end();
-
-                try module.makeBinFileExecutable();
-                break :x try std.ChildProcess.exec(.{
-                    .allocator = allocator,
-                    .argv = &[_][]const u8{"./a.out"},
-                    .cwd_dir = tmp.dir,
-                });
-            };
-            defer allocator.free(exec_result.stdout);
-            defer allocator.free(exec_result.stderr);
-            switch (exec_result.term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        std.debug.warn("elf file exited with code {}\n", .{code});
-                        return error.BinaryBadExitCode;
+                .Execution => |expected_stdout| {
+                    var exec_result = x: {
+                        var exec_node = update_node.start("execute", null);
+                        exec_node.activate();
+                        defer exec_node.end();
+
+                        try module.makeBinFileExecutable();
+
+                        const exe_path = try std.fmt.allocPrint(allocator, "." ++ std.fs.path.sep_str ++ "{}", .{bin_name});
+                        defer allocator.free(exe_path);
+
+                        break :x try std.ChildProcess.exec(.{
+                            .allocator = allocator,
+                            .argv = &[_][]const u8{exe_path},
+                            .cwd_dir = tmp.dir,
+                        });
+                    };
+                    defer allocator.free(exec_result.stdout);
+                    defer allocator.free(exec_result.stderr);
+                    switch (exec_result.term) {
+                        .Exited => |code| {
+                            if (code != 0) {
+                                std.debug.warn("elf file exited with code {}\n", .{code});
+                                return error.BinaryBadExitCode;
+                            }
+                        },
+                        else => return error.BinaryCrashed,
+                    }
+                    if (!std.mem.eql(u8, expected_stdout, exec_result.stdout)) {
+                        std.debug.panic(
+                            "update index {}, mismatched stdout\n====Expected (len={}):====\n{}\n====Actual (len={}):====\n{}\n========\n",
+                            .{ update_index, expected_stdout.len, expected_stdout, exec_result.stdout.len, exec_result.stdout },
+                        );
                     }
                 },
-                else => return error.BinaryCrashed,
-            }
-            const expected_stdout = case.expected_stdout_list[i];
-            if (!std.mem.eql(u8, expected_stdout, exec_result.stdout)) {
-                std.debug.panic(
-                    "update index {}, mismatched stdout\n====Expected (len={}):====\n{}\n====Actual (len={}):====\n{}\n========\n",
-                    .{ i, expected_stdout.len, expected_stdout, exec_result.stdout.len, exec_result.stdout },
-                );
             }
         }
     }
test/stage2/compare_output.zig
@@ -1,28 +1,121 @@
 const std = @import("std");
 const TestContext = @import("../../src-self-hosted/test.zig").TestContext;
+// self-hosted does not yet support PE executable files / COFF object files
+// or mach-o files. So we do these test cases cross compiling for x86_64-linux.
+const linux_x64 = std.zig.CrossTarget{
+    .cpu_arch = .x86_64,
+    .os_tag = .linux,
+};
 
 pub fn addCases(ctx: *TestContext) !void {
-    // TODO: re-enable these tests.
-    // https://github.com/ziglang/zig/issues/1364
+    if (std.Target.current.os.tag != .linux or
+        std.Target.current.cpu.arch != .x86_64)
+    {
+        // TODO implement self-hosted PE (.exe file) linking
+        // TODO implement more ZIR so we don't depend on x86_64-linux
+        return;
+    }
 
-    //// hello world
-    //try ctx.testCompareOutputLibC(
-    //    \\extern fn puts([*]const u8) void;
-    //    \\pub export fn main() c_int {
-    //    \\    puts("Hello, world!");
-    //    \\    return 0;
-    //    \\}
-    //, "Hello, world!" ++ std.cstr.line_sep);
-
-    //// function calling another function
-    //try ctx.testCompareOutputLibC(
-    //    \\extern fn puts(s: [*]const u8) void;
-    //    \\pub export fn main() c_int {
-    //    \\    return foo("OK");
-    //    \\}
-    //    \\fn foo(s: [*]const u8) c_int {
-    //    \\    puts(s);
-    //    \\    return 0;
-    //    \\}
-    //, "OK" ++ std.cstr.line_sep);
+    {
+        var case = ctx.addExe("hello world with updates", linux_x64);
+        // Regular old hello world
+        case.addCompareOutput(
+            \\export fn _start() noreturn {
+            \\    print();
+            \\
+            \\    exit();
+            \\}
+            \\
+            \\fn print() void {
+            \\    asm volatile ("syscall"
+            \\        :
+            \\        : [number] "{rax}" (1),
+            \\          [arg1] "{rdi}" (1),
+            \\          [arg2] "{rsi}" (@ptrToInt("Hello, World!\n")),
+            \\          [arg3] "{rdx}" (14)
+            \\        : "rcx", "r11", "memory"
+            \\    );
+            \\    return;
+            \\}
+            \\
+            \\fn exit() noreturn {
+            \\    asm volatile ("syscall"
+            \\        :
+            \\        : [number] "{rax}" (231),
+            \\          [arg1] "{rdi}" (0)
+            \\        : "rcx", "r11", "memory"
+            \\    );
+            \\    unreachable;
+            \\}
+        ,
+            "Hello, World!\n",
+        );
+        // Now change the message only
+        case.addCompareOutput(
+            \\export fn _start() noreturn {
+            \\    print();
+            \\
+            \\    exit();
+            \\}
+            \\
+            \\fn print() void {
+            \\    asm volatile ("syscall"
+            \\        :
+            \\        : [number] "{rax}" (1),
+            \\          [arg1] "{rdi}" (1),
+            \\          [arg2] "{rsi}" (@ptrToInt("What is up? This is a longer message that will force the data to be relocated in virtual address space.\n")),
+            \\          [arg3] "{rdx}" (104)
+            \\        : "rcx", "r11", "memory"
+            \\    );
+            \\    return;
+            \\}
+            \\
+            \\fn exit() noreturn {
+            \\    asm volatile ("syscall"
+            \\        :
+            \\        : [number] "{rax}" (231),
+            \\          [arg1] "{rdi}" (0)
+            \\        : "rcx", "r11", "memory"
+            \\    );
+            \\    unreachable;
+            \\}
+        ,
+            "What is up? This is a longer message that will force the data to be relocated in virtual address space.\n",
+        );
+        // Now we print it twice.
+        case.addCompareOutput(
+            \\export fn _start() noreturn {
+            \\    print();
+            \\    print();
+            \\
+            \\    exit();
+            \\}
+            \\
+            \\fn print() void {
+            \\    asm volatile ("syscall"
+            \\        :
+            \\        : [number] "{rax}" (1),
+            \\          [arg1] "{rdi}" (1),
+            \\          [arg2] "{rsi}" (@ptrToInt("What is up? This is a longer message that will force the data to be relocated in virtual address space.\n")),
+            \\          [arg3] "{rdx}" (104)
+            \\        : "rcx", "r11", "memory"
+            \\    );
+            \\    return;
+            \\}
+            \\
+            \\fn exit() noreturn {
+            \\    asm volatile ("syscall"
+            \\        :
+            \\        : [number] "{rax}" (231),
+            \\          [arg1] "{rdi}" (0)
+            \\        : "rcx", "r11", "memory"
+            \\    );
+            \\    unreachable;
+            \\}
+        ,
+            \\What is up? This is a longer message that will force the data to be relocated in virtual address space.
+            \\What is up? This is a longer message that will force the data to be relocated in virtual address space.
+            \\
+        );
+    }
 }
test/stage2/zir.zig
@@ -86,7 +86,7 @@ pub fn addCases(ctx: *TestContext) void {
     );
 
     {
-        var case = ctx.addZIRMulti("reference cycle with compile error in the cycle", linux_x64);
+        var case = ctx.addObjZIR("reference cycle with compile error in the cycle", linux_x64);
         case.addTransform(
             \\@void = primitive(void)
             \\@fnty = fntype([], @void, cc=C)
@@ -207,109 +207,101 @@ pub fn addCases(ctx: *TestContext) void {
         return;
     }
 
-    ctx.addZIRCompareOutput(
-        "hello world ZIR",
-        &[_][]const u8{
-            \\@noreturn = primitive(noreturn)
-            \\@void = primitive(void)
-            \\@usize = primitive(usize)
-            \\@0 = int(0)
-            \\@1 = int(1)
-            \\@2 = int(2)
-            \\@3 = int(3)
-            \\
-            \\@msg = str("Hello, world!\n")
-            \\
-            \\@start_fnty = fntype([], @noreturn, cc=Naked)
-            \\@start = fn(@start_fnty, {
-            \\  %SYS_exit_group = int(231)
-            \\  %exit_code = as(@usize, @0)
-            \\
-            \\  %syscall = str("syscall")
-            \\  %sysoutreg = str("={rax}")
-            \\  %rax = str("{rax}")
-            \\  %rdi = str("{rdi}")
-            \\  %rcx = str("rcx")
-            \\  %rdx = str("{rdx}")
-            \\  %rsi = str("{rsi}")
-            \\  %r11 = str("r11")
-            \\  %memory = str("memory")
-            \\
-            \\  %SYS_write = as(@usize, @1)
-            \\  %STDOUT_FILENO = as(@usize, @1)
-            \\
-            \\  %msg_addr = ptrtoint(@msg)
-            \\
-            \\  %len_name = str("len")
-            \\  %msg_len_ptr = fieldptr(@msg, %len_name)
-            \\  %msg_len = deref(%msg_len_ptr)
-            \\  %rc_write = asm(%syscall, @usize,
-            \\    volatile=1,
-            \\    output=%sysoutreg,
-            \\    inputs=[%rax, %rdi, %rsi, %rdx],
-            \\    clobbers=[%rcx, %r11, %memory],
-            \\    args=[%SYS_write, %STDOUT_FILENO, %msg_addr, %msg_len])
-            \\
-            \\  %rc_exit = asm(%syscall, @usize,
-            \\    volatile=1,
-            \\    output=%sysoutreg,
-            \\    inputs=[%rax, %rdi],
-            \\    clobbers=[%rcx, %r11, %memory],
-            \\    args=[%SYS_exit_group, %exit_code])
-            \\
-            \\  %99 = unreachable()
-            \\});
-            \\
-            \\@9 = str("_start")
-            \\@11 = export(@9, "start")
-        },
-        &[_][]const u8{
-            \\Hello, world!
-            \\
-        },
+    ctx.addZIRCompareOutput("hello world ZIR",
+        \\@noreturn = primitive(noreturn)
+        \\@void = primitive(void)
+        \\@usize = primitive(usize)
+        \\@0 = int(0)
+        \\@1 = int(1)
+        \\@2 = int(2)
+        \\@3 = int(3)
+        \\
+        \\@msg = str("Hello, world!\n")
+        \\
+        \\@start_fnty = fntype([], @noreturn, cc=Naked)
+        \\@start = fn(@start_fnty, {
+        \\  %SYS_exit_group = int(231)
+        \\  %exit_code = as(@usize, @0)
+        \\
+        \\  %syscall = str("syscall")
+        \\  %sysoutreg = str("={rax}")
+        \\  %rax = str("{rax}")
+        \\  %rdi = str("{rdi}")
+        \\  %rcx = str("rcx")
+        \\  %rdx = str("{rdx}")
+        \\  %rsi = str("{rsi}")
+        \\  %r11 = str("r11")
+        \\  %memory = str("memory")
+        \\
+        \\  %SYS_write = as(@usize, @1)
+        \\  %STDOUT_FILENO = as(@usize, @1)
+        \\
+        \\  %msg_addr = ptrtoint(@msg)
+        \\
+        \\  %len_name = str("len")
+        \\  %msg_len_ptr = fieldptr(@msg, %len_name)
+        \\  %msg_len = deref(%msg_len_ptr)
+        \\  %rc_write = asm(%syscall, @usize,
+        \\    volatile=1,
+        \\    output=%sysoutreg,
+        \\    inputs=[%rax, %rdi, %rsi, %rdx],
+        \\    clobbers=[%rcx, %r11, %memory],
+        \\    args=[%SYS_write, %STDOUT_FILENO, %msg_addr, %msg_len])
+        \\
+        \\  %rc_exit = asm(%syscall, @usize,
+        \\    volatile=1,
+        \\    output=%sysoutreg,
+        \\    inputs=[%rax, %rdi],
+        \\    clobbers=[%rcx, %r11, %memory],
+        \\    args=[%SYS_exit_group, %exit_code])
+        \\
+        \\  %99 = unreachable()
+        \\});
+        \\
+        \\@9 = str("_start")
+        \\@11 = export(@9, "start")
+    ,
+        \\Hello, world!
+        \\
     );
 
-    ctx.addZIRCompareOutput(
-        "function call with no args no return value",
-        &[_][]const u8{
-            \\@noreturn = primitive(noreturn)
-            \\@void = primitive(void)
-            \\@usize = primitive(usize)
-            \\@0 = int(0)
-            \\@1 = int(1)
-            \\@2 = int(2)
-            \\@3 = int(3)
-            \\
-            \\@exit0_fnty = fntype([], @noreturn)
-            \\@exit0 = fn(@exit0_fnty, {
-            \\  %SYS_exit_group = int(231)
-            \\  %exit_code = as(@usize, @0)
-            \\
-            \\  %syscall = str("syscall")
-            \\  %sysoutreg = str("={rax}")
-            \\  %rax = str("{rax}")
-            \\  %rdi = str("{rdi}")
-            \\  %rcx = str("rcx")
-            \\  %r11 = str("r11")
-            \\  %memory = str("memory")
-            \\
-            \\  %rc = asm(%syscall, @usize,
-            \\    volatile=1,
-            \\    output=%sysoutreg,
-            \\    inputs=[%rax, %rdi],
-            \\    clobbers=[%rcx, %r11, %memory],
-            \\    args=[%SYS_exit_group, %exit_code])
-            \\
-            \\  %99 = unreachable()
-            \\});
-            \\
-            \\@start_fnty = fntype([], @noreturn, cc=Naked)
-            \\@start = fn(@start_fnty, {
-            \\  %0 = call(@exit0, [])
-            \\})
-            \\@9 = str("_start")
-            \\@11 = export(@9, "start")
-        },
-        &[_][]const u8{""},
-    );
+    ctx.addZIRCompareOutput("function call with no args no return value",
+        \\@noreturn = primitive(noreturn)
+        \\@void = primitive(void)
+        \\@usize = primitive(usize)
+        \\@0 = int(0)
+        \\@1 = int(1)
+        \\@2 = int(2)
+        \\@3 = int(3)
+        \\
+        \\@exit0_fnty = fntype([], @noreturn)
+        \\@exit0 = fn(@exit0_fnty, {
+        \\  %SYS_exit_group = int(231)
+        \\  %exit_code = as(@usize, @0)
+        \\
+        \\  %syscall = str("syscall")
+        \\  %sysoutreg = str("={rax}")
+        \\  %rax = str("{rax}")
+        \\  %rdi = str("{rdi}")
+        \\  %rcx = str("rcx")
+        \\  %r11 = str("r11")
+        \\  %memory = str("memory")
+        \\
+        \\  %rc = asm(%syscall, @usize,
+        \\    volatile=1,
+        \\    output=%sysoutreg,
+        \\    inputs=[%rax, %rdi],
+        \\    clobbers=[%rcx, %r11, %memory],
+        \\    args=[%SYS_exit_group, %exit_code])
+        \\
+        \\  %99 = unreachable()
+        \\});
+        \\
+        \\@start_fnty = fntype([], @noreturn, cc=Naked)
+        \\@start = fn(@start_fnty, {
+        \\  %0 = call(@exit0, [])
+        \\})
+        \\@9 = str("_start")
+        \\@11 = export(@9, "start")
+    , "");
 }