Commit ef91b11295

Andrew Kelley <andrew@ziglang.org>
2020-07-20 22:11:07
stage2: register allocator processes operand deaths
also rework the IR data structures
1 parent a8065a0
Changed files (8)
src-self-hosted/codegen/c.zig
@@ -92,9 +92,9 @@ fn genFn(file: *C, decl: *Decl) !void {
         for (instructions) |inst| {
             try writer.writeAll("\n\t");
             switch (inst.tag) {
-                .assembly => try genAsm(file, inst.cast(Inst.Assembly).?, decl),
-                .call => try genCall(file, inst.cast(Inst.Call).?, decl),
-                .ret => try genRet(file, inst.cast(Inst.Ret).?, decl, tv.ty.fnReturnType()),
+                .assembly => try genAsm(file, inst.castTag(.assembly).?, decl),
+                .call => try genCall(file, inst.castTag(.call).?, decl),
+                .ret => try genRet(file, inst.castTag(.ret).?, decl, tv.ty.fnReturnType()),
                 .retvoid => try file.main.writer().print("return;", .{}),
                 else => |e| return file.fail(decl.src(), "TODO implement C codegen for {}", .{e}),
             }
@@ -105,9 +105,9 @@ fn genFn(file: *C, decl: *Decl) !void {
     try writer.writeAll("}\n\n");
 }
 
-fn genRet(file: *C, inst: *Inst.Ret, decl: *Decl, expected_return_type: Type) !void {
+fn genRet(file: *C, inst: *Inst.UnOp, decl: *Decl, expected_return_type: Type) !void {
     const writer = file.main.writer();
-    const ret_value = inst.args.operand;
+    const ret_value = inst.operand;
     const value = ret_value.value().?;
     if (expected_return_type.eql(ret_value.ty))
         return file.fail(decl.src(), "TODO return {}", .{expected_return_type})
@@ -126,7 +126,7 @@ fn genRet(file: *C, inst: *Inst.Ret, decl: *Decl, expected_return_type: Type) !v
 fn genCall(file: *C, inst: *Inst.Call, decl: *Decl) !void {
     const writer = file.main.writer();
     const header = file.header.writer();
-    if (inst.args.func.cast(Inst.Constant)) |func_inst| {
+    if (inst.func.castTag(.constant)) |func_inst| {
         if (func_inst.val.cast(Value.Payload.Function)) |func_val| {
             const target = func_val.func.owner_decl;
             const target_ty = target.typed_value.most_recent.typed_value.ty;
@@ -144,7 +144,7 @@ fn genCall(file: *C, inst: *Inst.Call, decl: *Decl) !void {
         } else {
             return file.fail(decl.src(), "TODO non-function call target?", .{});
         }
-        if (inst.args.args.len != 0) {
+        if (inst.args.len != 0) {
             return file.fail(decl.src(), "TODO function arguments", .{});
         }
     } else {
@@ -152,14 +152,13 @@ fn genCall(file: *C, inst: *Inst.Call, decl: *Decl) !void {
     }
 }
 
-fn genAsm(file: *C, inst: *Inst.Assembly, decl: *Decl) !void {
-    const as = inst.args;
+fn genAsm(file: *C, as: *Inst.Assembly, decl: *Decl) !void {
     const writer = file.main.writer();
     for (as.inputs) |i, index| {
         if (i[0] == '{' and i[i.len - 1] == '}') {
             const reg = i[1 .. i.len - 1];
             const arg = as.args[index];
-            if (arg.cast(Inst.Constant)) |c| {
+            if (arg.castTag(.constant)) |c| {
                 if (c.val.tag() == .int_u64) {
                     try writer.writeAll("register ");
                     try renderType(file, writer, arg.ty, decl.src());
@@ -190,7 +189,7 @@ fn genAsm(file: *C, inst: *Inst.Assembly, decl: *Decl) !void {
                 if (index > 0) {
                     try writer.writeAll(", ");
                 }
-                if (arg.cast(Inst.Constant)) |c| {
+                if (arg.castTag(.constant)) |c| {
                     try writer.print("\"\"({}_constant)", .{reg});
                 } else {
                     // This is blocked by the earlier test
src-self-hosted/astgen.zig
@@ -173,8 +173,8 @@ fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.In
     const if_src = tree.token_locs[if_node.if_token].start;
     const condbr = try mod.addZIRInstSpecial(&block_scope.base, if_src, zir.Inst.CondBr, .{
         .condition = cond,
-        .true_body = undefined, // populated below
-        .false_body = undefined, // populated below
+        .then_body = undefined, // populated below
+        .else_body = undefined, // populated below
     }, .{});
 
     const block = try mod.addZIRInstBlock(scope, if_src, .{
@@ -196,7 +196,7 @@ fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.In
             .operand = then_result,
         }, .{});
     }
-    condbr.positionals.true_body = .{
+    condbr.positionals.then_body = .{
         .instructions = try then_scope.arena.dupe(*zir.Inst, then_scope.instructions.items),
     };
 
@@ -225,7 +225,7 @@ fn ifExpr(mod: *Module, scope: *Scope, if_node: *ast.Node.If) InnerError!*zir.In
             .block = block,
         }, .{});
     }
-    condbr.positionals.false_body = .{
+    condbr.positionals.else_body = .{
         .instructions = try else_scope.arena.dupe(*zir.Inst, else_scope.instructions.items),
     };
 
src-self-hosted/codegen.zig
@@ -290,6 +290,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             next_stack_offset: u32 = 0,
 
             fn markRegUsed(self: *Branch, reg: Register) void {
+                if (FreeRegInt == u0) return;
                 const index = reg.allocIndex() orelse return;
                 const ShiftInt = std.math.Log2Int(FreeRegInt);
                 const shift = @intCast(ShiftInt, index);
@@ -297,6 +298,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             }
 
             fn markRegFree(self: *Branch, reg: Register) void {
+                if (FreeRegInt == u0) return;
                 const index = reg.allocIndex() orelse return;
                 const ShiftInt = std.math.Log2Int(FreeRegInt);
                 const shift = @intCast(ShiftInt, index);
@@ -407,40 +409,64 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             for (body.instructions) |inst| {
                 const new_inst = try self.genFuncInst(inst);
                 try inst_table.putNoClobber(self.gpa, inst, new_inst);
-                // TODO process operand deaths
+
+                var i: ir.Inst.DeathsBitIndex = 0;
+                while (inst.getOperand(i)) |operand| : (i += 1) {
+                    if (inst.operandDies(i))
+                        self.processDeath(operand);
+                }
+            }
+        }
+
+        fn processDeath(self: *Self, inst: *ir.Inst) void {
+            const branch = &self.branch_stack.items[self.branch_stack.items.len - 1];
+            const entry = branch.inst_table.getEntry(inst) orelse return;
+            const prev_value = entry.value;
+            entry.value = .dead;
+            switch (prev_value) {
+                .register => |reg| {
+                    _ = branch.registers.remove(reg);
+                    branch.markRegFree(reg);
+                },
+                else => {}, // TODO process stack allocation death
             }
         }
 
         fn genFuncInst(self: *Self, inst: *ir.Inst) !MCValue {
             switch (inst.tag) {
-                .add => return self.genAdd(inst.cast(ir.Inst.Add).?),
-                .arg => return self.genArg(inst.cast(ir.Inst.Arg).?),
-                .assembly => return self.genAsm(inst.cast(ir.Inst.Assembly).?),
-                .bitcast => return self.genBitCast(inst.cast(ir.Inst.BitCast).?),
-                .block => return self.genBlock(inst.cast(ir.Inst.Block).?),
-                .br => return self.genBr(inst.cast(ir.Inst.Br).?),
+                .add => return self.genAdd(inst.castTag(.add).?),
+                .arg => return self.genArg(inst.castTag(.arg).?),
+                .assembly => return self.genAsm(inst.castTag(.assembly).?),
+                .bitcast => return self.genBitCast(inst.castTag(.bitcast).?),
+                .block => return self.genBlock(inst.castTag(.block).?),
+                .br => return self.genBr(inst.castTag(.br).?),
                 .breakpoint => return self.genBreakpoint(inst.src),
-                .brvoid => return self.genBrVoid(inst.cast(ir.Inst.BrVoid).?),
-                .call => return self.genCall(inst.cast(ir.Inst.Call).?),
-                .cmp => return self.genCmp(inst.cast(ir.Inst.Cmp).?),
-                .condbr => return self.genCondBr(inst.cast(ir.Inst.CondBr).?),
+                .brvoid => return self.genBrVoid(inst.castTag(.brvoid).?),
+                .call => return self.genCall(inst.castTag(.call).?),
+                .cmp_lt => return self.genCmp(inst.castTag(.cmp_lt).?, .lt),
+                .cmp_lte => return self.genCmp(inst.castTag(.cmp_lte).?, .lte),
+                .cmp_eq => return self.genCmp(inst.castTag(.cmp_eq).?, .eq),
+                .cmp_gte => return self.genCmp(inst.castTag(.cmp_gte).?, .gte),
+                .cmp_gt => return self.genCmp(inst.castTag(.cmp_gt).?, .gt),
+                .cmp_neq => return self.genCmp(inst.castTag(.cmp_neq).?, .neq),
+                .condbr => return self.genCondBr(inst.castTag(.condbr).?),
                 .constant => unreachable, // excluded from function bodies
-                .isnonnull => return self.genIsNonNull(inst.cast(ir.Inst.IsNonNull).?),
-                .isnull => return self.genIsNull(inst.cast(ir.Inst.IsNull).?),
-                .ptrtoint => return self.genPtrToInt(inst.cast(ir.Inst.PtrToInt).?),
-                .ret => return self.genRet(inst.cast(ir.Inst.Ret).?),
-                .retvoid => return self.genRetVoid(inst.cast(ir.Inst.RetVoid).?),
-                .sub => return self.genSub(inst.cast(ir.Inst.Sub).?),
+                .isnonnull => return self.genIsNonNull(inst.castTag(.isnonnull).?),
+                .isnull => return self.genIsNull(inst.castTag(.isnull).?),
+                .ptrtoint => return self.genPtrToInt(inst.castTag(.ptrtoint).?),
+                .ret => return self.genRet(inst.castTag(.ret).?),
+                .retvoid => return self.genRetVoid(inst.castTag(.retvoid).?),
+                .sub => return self.genSub(inst.castTag(.sub).?),
                 .unreach => return MCValue{ .unreach = {} },
-                .not => return self.genNot(inst.cast(ir.Inst.Not).?),
+                .not => return self.genNot(inst.castTag(.not).?),
             }
         }
 
-        fn genNot(self: *Self, inst: *ir.Inst.Not) !MCValue {
+        fn genNot(self: *Self, inst: *ir.Inst.UnOp) !MCValue {
             // No side effects, so if it's unreferenced, do nothing.
             if (inst.base.isUnused())
                 return MCValue.dead;
-            const operand = try self.resolveInst(inst.args.operand);
+            const operand = try self.resolveInst(inst.operand);
             switch (operand) {
                 .dead => unreachable,
                 .unreach => unreachable,
@@ -473,36 +499,36 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
                         .base = .{
                             .tag = .constant,
                             .deaths = 0,
-                            .ty = inst.args.operand.ty,
-                            .src = inst.args.operand.src,
+                            .ty = inst.operand.ty,
+                            .src = inst.operand.src,
                         },
                         .val = Value.initTag(.bool_true),
                     };
-                    return try self.genX8664BinMath(&inst.base, inst.args.operand, &imm.base, 6, 0x30);
+                    return try self.genX8664BinMath(&inst.base, inst.operand, &imm.base, 6, 0x30);
                 },
                 else => return self.fail(inst.base.src, "TODO implement NOT for {}", .{self.target.cpu.arch}),
             }
         }
 
-        fn genAdd(self: *Self, inst: *ir.Inst.Add) !MCValue {
+        fn genAdd(self: *Self, inst: *ir.Inst.BinOp) !MCValue {
             // No side effects, so if it's unreferenced, do nothing.
             if (inst.base.isUnused())
                 return MCValue.dead;
             switch (arch) {
                 .x86_64 => {
-                    return try self.genX8664BinMath(&inst.base, inst.args.lhs, inst.args.rhs, 0, 0x00);
+                    return try self.genX8664BinMath(&inst.base, inst.lhs, inst.rhs, 0, 0x00);
                 },
                 else => return self.fail(inst.base.src, "TODO implement add for {}", .{self.target.cpu.arch}),
             }
         }
 
-        fn genSub(self: *Self, inst: *ir.Inst.Sub) !MCValue {
+        fn genSub(self: *Self, inst: *ir.Inst.BinOp) !MCValue {
             // No side effects, so if it's unreferenced, do nothing.
             if (inst.base.isUnused())
                 return MCValue.dead;
             switch (arch) {
                 .x86_64 => {
-                    return try self.genX8664BinMath(&inst.base, inst.args.lhs, inst.args.rhs, 5, 0x28);
+                    return try self.genX8664BinMath(&inst.base, inst.lhs, inst.rhs, 5, 0x28);
                 },
                 else => return self.fail(inst.base.src, "TODO implement sub for {}", .{self.target.cpu.arch}),
             }
@@ -625,7 +651,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             }
         }
 
-        fn genArg(self: *Self, inst: *ir.Inst.Arg) !MCValue {
+        fn genArg(self: *Self, inst: *ir.Inst.NoOp) !MCValue {
             if (FreeRegInt == u0) {
                 return self.fail(inst.base.src, "TODO implement Register enum for {}", .{self.target.cpu.arch});
             }
@@ -659,7 +685,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
         }
 
         fn genCall(self: *Self, inst: *ir.Inst.Call) !MCValue {
-            const fn_ty = inst.args.func.ty;
+            const fn_ty = inst.func.ty;
             const cc = fn_ty.fnCallingConvention();
             const param_types = try self.gpa.alloc(Type, fn_ty.fnParamLen());
             defer self.gpa.free(param_types);
@@ -671,8 +697,8 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             switch (arch) {
                 .x86_64 => {
                     for (mc_args) |mc_arg, arg_i| {
-                        const arg = inst.args.args[arg_i];
-                        const arg_mcv = try self.resolveInst(inst.args.args[arg_i]);
+                        const arg = inst.args[arg_i];
+                        const arg_mcv = try self.resolveInst(inst.args[arg_i]);
                         switch (mc_arg) {
                             .none => continue,
                             .register => |reg| {
@@ -694,7 +720,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
                         }
                     }
 
-                    if (inst.args.func.cast(ir.Inst.Constant)) |func_inst| {
+                    if (inst.func.cast(ir.Inst.Constant)) |func_inst| {
                         if (func_inst.val.cast(Value.Payload.Function)) |func_val| {
                             const func = func_val.func;
                             const got = &self.bin_file.program_headers.items[self.bin_file.phdr_got_index.?];
@@ -742,16 +768,16 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             return .unreach;
         }
 
-        fn genRet(self: *Self, inst: *ir.Inst.Ret) !MCValue {
-            const operand = try self.resolveInst(inst.args.operand);
+        fn genRet(self: *Self, inst: *ir.Inst.UnOp) !MCValue {
+            const operand = try self.resolveInst(inst.operand);
             return self.ret(inst.base.src, operand);
         }
 
-        fn genRetVoid(self: *Self, inst: *ir.Inst.RetVoid) !MCValue {
+        fn genRetVoid(self: *Self, inst: *ir.Inst.NoOp) !MCValue {
             return self.ret(inst.base.src, .none);
         }
 
-        fn genCmp(self: *Self, inst: *ir.Inst.Cmp) !MCValue {
+        fn genCmp(self: *Self, inst: *ir.Inst.BinOp, op: std.math.CompareOperator) !MCValue {
             // No side effects, so if it's unreferenced, do nothing.
             if (inst.base.isUnused())
                 return MCValue.dead;
@@ -759,25 +785,25 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
                 .x86_64 => {
                     try self.code.ensureCapacity(self.code.items.len + 8);
 
-                    const lhs = try self.resolveInst(inst.args.lhs);
-                    const rhs = try self.resolveInst(inst.args.rhs);
+                    const lhs = try self.resolveInst(inst.lhs);
+                    const rhs = try self.resolveInst(inst.rhs);
 
                     // There are 2 operands, destination and source.
                     // Either one, but not both, can be a memory operand.
                     // Source operand can be an immediate, 8 bits or 32 bits.
                     const dst_mcv = if (lhs.isImmediate() or (lhs.isMemory() and rhs.isMemory()))
-                        try self.copyToNewRegister(inst.args.lhs)
+                        try self.copyToNewRegister(inst.lhs)
                     else
                         lhs;
                     // This instruction supports only signed 32-bit immediates at most.
-                    const src_mcv = try self.limitImmediateType(inst.args.rhs, i32);
+                    const src_mcv = try self.limitImmediateType(inst.rhs, i32);
 
                     try self.genX8664BinMathCode(inst.base.src, dst_mcv, src_mcv, 7, 0x38);
-                    const info = inst.args.lhs.ty.intInfo(self.target.*);
+                    const info = inst.lhs.ty.intInfo(self.target.*);
                     if (info.signed) {
-                        return MCValue{ .compare_flags_signed = inst.args.op };
+                        return MCValue{ .compare_flags_signed = op };
                     } else {
-                        return MCValue{ .compare_flags_unsigned = inst.args.op };
+                        return MCValue{ .compare_flags_unsigned = op };
                     }
                 },
                 else => return self.fail(inst.base.src, "TODO implement cmp for {}", .{self.target.cpu.arch}),
@@ -789,7 +815,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
                 .x86_64 => {
                     try self.code.ensureCapacity(self.code.items.len + 6);
 
-                    const cond = try self.resolveInst(inst.args.condition);
+                    const cond = try self.resolveInst(inst.condition);
                     switch (cond) {
                         .compare_flags_signed => |cmp_op| {
                             // Here we map to the opposite opcode because the jump is to the false branch.
@@ -838,19 +864,19 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             self.code.appendSliceAssumeCapacity(&[_]u8{ 0x0f, opcode });
             const reloc = Reloc{ .rel32 = self.code.items.len };
             self.code.items.len += 4;
-            try self.genBody(inst.args.true_body);
+            try self.genBody(inst.then_body);
             try self.performReloc(inst.base.src, reloc);
-            try self.genBody(inst.args.false_body);
+            try self.genBody(inst.else_body);
             return MCValue.unreach;
         }
 
-        fn genIsNull(self: *Self, inst: *ir.Inst.IsNull) !MCValue {
+        fn genIsNull(self: *Self, inst: *ir.Inst.UnOp) !MCValue {
             switch (arch) {
                 else => return self.fail(inst.base.src, "TODO implement isnull for {}", .{self.target.cpu.arch}),
             }
         }
 
-        fn genIsNonNull(self: *Self, inst: *ir.Inst.IsNonNull) !MCValue {
+        fn genIsNonNull(self: *Self, inst: *ir.Inst.UnOp) !MCValue {
             // Here you can specialize this instruction if it makes sense to, otherwise the default
             // will call genIsNull and invert the result.
             switch (arch) {
@@ -864,7 +890,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             }
             // A block is nothing but a setup to be able to jump to the end.
             defer inst.codegen.relocs.deinit(self.gpa);
-            try self.genBody(inst.args.body);
+            try self.genBody(inst.body);
 
             for (inst.codegen.relocs.items) |reloc| try self.performReloc(inst.base.src, reloc);
 
@@ -883,17 +909,17 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
         }
 
         fn genBr(self: *Self, inst: *ir.Inst.Br) !MCValue {
-            if (!inst.args.operand.ty.hasCodeGenBits())
-                return self.brVoid(inst.base.src, inst.args.block);
+            if (!inst.operand.ty.hasCodeGenBits())
+                return self.brVoid(inst.base.src, inst.block);
 
-            const operand = try self.resolveInst(inst.args.operand);
+            const operand = try self.resolveInst(inst.operand);
             switch (arch) {
                 else => return self.fail(inst.base.src, "TODO implement br for {}", .{self.target.cpu.arch}),
             }
         }
 
         fn genBrVoid(self: *Self, inst: *ir.Inst.BrVoid) !MCValue {
-            return self.brVoid(inst.base.src, inst.args.block);
+            return self.brVoid(inst.base.src, inst.block);
         }
 
         fn brVoid(self: *Self, src: usize, block: *ir.Inst.Block) !MCValue {
@@ -915,29 +941,29 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
         }
 
         fn genAsm(self: *Self, inst: *ir.Inst.Assembly) !MCValue {
-            if (!inst.args.is_volatile and inst.base.isUnused())
+            if (!inst.is_volatile and inst.base.isUnused())
                 return MCValue.dead;
             if (arch != .x86_64 and arch != .i386) {
                 return self.fail(inst.base.src, "TODO implement inline asm support for more architectures", .{});
             }
-            for (inst.args.inputs) |input, i| {
+            for (inst.inputs) |input, i| {
                 if (input.len < 3 or input[0] != '{' or input[input.len - 1] != '}') {
                     return self.fail(inst.base.src, "unrecognized asm input constraint: '{}'", .{input});
                 }
                 const reg_name = input[1 .. input.len - 1];
                 const reg = parseRegName(reg_name) orelse
                     return self.fail(inst.base.src, "unrecognized register: '{}'", .{reg_name});
-                const arg = try self.resolveInst(inst.args.args[i]);
+                const arg = try self.resolveInst(inst.args[i]);
                 try self.genSetReg(inst.base.src, reg, arg);
             }
 
-            if (mem.eql(u8, inst.args.asm_source, "syscall")) {
+            if (mem.eql(u8, inst.asm_source, "syscall")) {
                 try self.code.appendSlice(&[_]u8{ 0x0f, 0x05 });
             } else {
                 return self.fail(inst.base.src, "TODO implement support for more x86 assembly instructions", .{});
             }
 
-            if (inst.args.output) |output| {
+            if (inst.output) |output| {
                 if (output.len < 4 or output[0] != '=' or output[1] != '{' or output[output.len - 1] != '}') {
                     return self.fail(inst.base.src, "unrecognized asm output constraint: '{}'", .{output});
                 }
@@ -1169,13 +1195,13 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             }
         }
 
-        fn genPtrToInt(self: *Self, inst: *ir.Inst.PtrToInt) !MCValue {
+        fn genPtrToInt(self: *Self, inst: *ir.Inst.UnOp) !MCValue {
             // no-op
-            return self.resolveInst(inst.args.ptr);
+            return self.resolveInst(inst.operand);
         }
 
-        fn genBitCast(self: *Self, inst: *ir.Inst.BitCast) !MCValue {
-            const operand = try self.resolveInst(inst.args.operand);
+        fn genBitCast(self: *Self, inst: *ir.Inst.UnOp) !MCValue {
+            const operand = try self.resolveInst(inst.operand);
             return operand;
         }
 
src-self-hosted/ir.zig
@@ -55,7 +55,12 @@ pub const Inst = struct {
         breakpoint,
         brvoid,
         call,
-        cmp,
+        cmp_lt,
+        cmp_lte,
+        cmp_eq,
+        cmp_gte,
+        cmp_gt,
+        cmp_neq,
         condbr,
         constant,
         isnonnull,
@@ -66,13 +71,80 @@ pub const Inst = struct {
         sub,
         unreach,
         not,
+
+        /// There is one-to-one correspondence between tag and type for now,
+        /// but this will not always be the case. For example, binary operations
+        /// such as + and - will have different tags but the same type.
+        pub fn Type(tag: Tag) type {
+            return switch (tag) {
+                .retvoid,
+                .unreach,
+                .arg,
+                .breakpoint,
+                => NoOp,
+
+                .ret,
+                .bitcast,
+                .not,
+                .isnonnull,
+                .isnull,
+                .ptrtoint,
+                => UnOp,
+
+                .add,
+                .sub,
+                .cmp_lt,
+                .cmp_lte,
+                .cmp_eq,
+                .cmp_gte,
+                .cmp_gt,
+                .cmp_neq,
+                => BinOp,
+
+                .assembly => Assembly,
+                .block => Block,
+                .br => Br,
+                .brvoid => BrVoid,
+                .call => Call,
+                .condbr => CondBr,
+                .constant => Constant,
+            };
+        }
+
+        pub fn fromCmpOp(op: std.math.CompareOperator) Tag {
+            return switch (op) {
+                .lt => .cmp_lt,
+                .lte => .cmp_lte,
+                .eq => .cmp_eq,
+                .gte => .cmp_gte,
+                .gt => .cmp_gt,
+                .neq => .cmp_neq,
+            };
+        }
     };
 
+    /// Prefer `castTag` to this.
     pub fn cast(base: *Inst, comptime T: type) ?*T {
-        if (base.tag != T.base_tag)
-            return null;
+        if (@hasField(T, "base_tag")) {
+            return base.castTag(T.base_tag);
+        }
+        inline for (@typeInfo(Tag).Enum.fields) |field| {
+            const tag = @intToEnum(Tag, field.value);
+            if (base.tag == tag) {
+                if (T == tag.Type()) {
+                    return @fieldParentPtr(T, "base", base);
+                }
+                return null;
+            }
+        }
+        unreachable;
+    }
 
-        return @fieldParentPtr(T, "base", base);
+    pub fn castTag(base: *Inst, comptime tag: Tag) ?*tag.Type() {
+        if (base.tag == tag) {
+            return @fieldParentPtr(tag.Type(), "base", base);
+        }
+        return null;
     }
 
     pub fn Args(comptime T: type) type {
@@ -88,186 +160,219 @@ pub const Inst = struct {
         return inst.val;
     }
 
-    pub const Add = struct {
-        pub const base_tag = Tag.add;
+    pub fn cmpOperator(base: *Inst) ?std.math.CompareOperator {
+        return switch (self.base.tag) {
+            .cmp_lt => .lt,
+            .cmp_lte => .lte,
+            .cmp_eq => .eq,
+            .cmp_gte => .gte,
+            .cmp_gt => .gt,
+            .cmp_neq => .neq,
+            else => null,
+        };
+    }
+
+    pub fn operandCount(base: *Inst) usize {
+        inline for (@typeInfo(Tag).Enum.fields) |field| {
+            const tag = @intToEnum(Tag, field.value);
+            if (tag == base.tag) {
+                return @fieldParentPtr(tag.Type(), "base", base).operandCount();
+            }
+        }
+        unreachable;
+    }
+
+    pub fn getOperand(base: *Inst, index: usize) ?*Inst {
+        inline for (@typeInfo(Tag).Enum.fields) |field| {
+            const tag = @intToEnum(Tag, field.value);
+            if (tag == base.tag) {
+                return @fieldParentPtr(tag.Type(), "base", base).getOperand(index);
+            }
+        }
+        unreachable;
+    }
+
+    pub const NoOp = struct {
         base: Inst,
 
-        args: struct {
-            lhs: *Inst,
-            rhs: *Inst,
-        },
+        pub fn operandCount(self: *const NoOp) usize {
+            return 0;
+        }
+        pub fn getOperand(self: *const NoOp, index: usize) ?*Inst {
+            return null;
+        }
     };
 
-    pub const Arg = struct {
-        pub const base_tag = Tag.arg;
+    pub const UnOp = struct {
         base: Inst,
-        args: void,
+        operand: *Inst,
+
+        pub fn operandCount(self: *const UnOp) usize {
+            return 1;
+        }
+        pub fn getOperand(self: *const UnOp, index: usize) ?*Inst {
+            if (index == 0)
+                return self.operand;
+            return null;
+        }
     };
 
-    pub const Assembly = struct {
-        pub const base_tag = Tag.assembly;
+    pub const BinOp = struct {
         base: Inst,
+        lhs: *Inst,
+        rhs: *Inst,
 
-        args: struct {
-            asm_source: []const u8,
-            is_volatile: bool,
-            output: ?[]const u8,
-            inputs: []const []const u8,
-            clobbers: []const []const u8,
-            args: []const *Inst,
-        },
+        pub fn operandCount(self: *const BinOp) usize {
+            return 2;
+        }
+        pub fn getOperand(self: *const BinOp, index: usize) ?*Inst {
+            var i = index;
+
+            if (i < 1)
+                return self.lhs;
+            i -= 1;
+
+            if (i < 1)
+                return self.rhs;
+            i -= 1;
+
+            return null;
+        }
     };
 
-    pub const BitCast = struct {
-        pub const base_tag = Tag.bitcast;
+    pub const Assembly = struct {
+        pub const base_tag = Tag.assembly;
 
         base: Inst,
-        args: struct {
-            operand: *Inst,
-        },
+        asm_source: []const u8,
+        is_volatile: bool,
+        output: ?[]const u8,
+        inputs: []const []const u8,
+        clobbers: []const []const u8,
+        args: []const *Inst,
+
+        pub fn operandCount(self: *const Assembly) usize {
+            return self.args.len;
+        }
+        pub fn getOperand(self: *const Assembly, index: usize) ?*Inst {
+            if (index < self.args.len)
+                return self.args[index];
+            return null;
+        }
     };
 
     pub const Block = struct {
         pub const base_tag = Tag.block;
+
         base: Inst,
-        args: struct {
-            body: Body,
-        },
+        body: Body,
         /// This memory is reserved for codegen code to do whatever it needs to here.
         codegen: codegen.BlockData = .{},
+
+        pub fn operandCount(self: *const Block) usize {
+            return 0;
+        }
+        pub fn getOperand(self: *const Block, index: usize) ?*Inst {
+            return null;
+        }
     };
 
     pub const Br = struct {
         pub const base_tag = Tag.br;
-        base: Inst,
-        args: struct {
-            block: *Block,
-            operand: *Inst,
-        },
-    };
 
-    pub const Breakpoint = struct {
-        pub const base_tag = Tag.breakpoint;
         base: Inst,
-        args: void,
+        block: *Block,
+        operand: *Inst,
+
+        pub fn operandCount(self: *const Br) usize {
+            return 0;
+        }
+        pub fn getOperand(self: *const Br, index: usize) ?*Inst {
+            if (index == 0)
+                return self.operand;
+            return null;
+        }
     };
 
     pub const BrVoid = struct {
         pub const base_tag = Tag.brvoid;
+
         base: Inst,
-        args: struct {
-            block: *Block,
-        },
+        block: *Block,
+
+        pub fn operandCount(self: *const BrVoid) usize {
+            return 0;
+        }
+        pub fn getOperand(self: *const BrVoid, index: usize) ?*Inst {
+            return null;
+        }
     };
 
     pub const Call = struct {
         pub const base_tag = Tag.call;
+
         base: Inst,
-        args: struct {
-            func: *Inst,
-            args: []const *Inst,
-        },
-    };
+        func: *Inst,
+        args: []const *Inst,
 
-    pub const Cmp = struct {
-        pub const base_tag = Tag.cmp;
+        pub fn operandCount(self: *const Call) usize {
+            return self.args.len + 1;
+        }
+        pub fn getOperand(self: *const Call, index: usize) ?*Inst {
+            var i = index;
 
-        base: Inst,
-        args: struct {
-            lhs: *Inst,
-            op: std.math.CompareOperator,
-            rhs: *Inst,
-        },
+            if (i < 1)
+                return self.func;
+            i -= 1;
+
+            if (i < self.args.len)
+                return self.args[i];
+            i -= self.args.len;
+
+            return null;
+        }
     };
 
     pub const CondBr = struct {
         pub const base_tag = Tag.condbr;
 
         base: Inst,
-        args: struct {
-            condition: *Inst,
-            true_body: Body,
-            false_body: Body,
-        },
+        condition: *Inst,
+        then_body: Body,
+        else_body: Body,
         /// Set of instructions whose lifetimes end at the start of one of the branches.
         /// The `true` branch is first: `deaths[0..true_death_count]`.
         /// The `false` branch is next: `(deaths + true_death_count)[..false_death_count]`.
         deaths: [*]*Inst = undefined,
         true_death_count: u32 = 0,
         false_death_count: u32 = 0,
-    };
 
-    pub const Not = struct {
-        pub const base_tag = Tag.not;
+        pub fn operandCount(self: *const CondBr) usize {
+            return 1;
+        }
+        pub fn getOperand(self: *const CondBr, index: usize) ?*Inst {
+            var i = index;
 
-        base: Inst,
-        args: struct {
-            operand: *Inst,
-        },
+            if (i < 1)
+                return self.condition;
+            i -= 1;
+
+            return null;
+        }
     };
 
     pub const Constant = struct {
         pub const base_tag = Tag.constant;
-        base: Inst,
-
-        val: Value,
-    };
-
-    pub const IsNonNull = struct {
-        pub const base_tag = Tag.isnonnull;
-
-        base: Inst,
-        args: struct {
-            operand: *Inst,
-        },
-    };
-
-    pub const IsNull = struct {
-        pub const base_tag = Tag.isnull;
 
         base: Inst,
-        args: struct {
-            operand: *Inst,
-        },
-    };
-
-    pub const PtrToInt = struct {
-        pub const base_tag = Tag.ptrtoint;
-
-        base: Inst,
-        args: struct {
-            ptr: *Inst,
-        },
-    };
-
-    pub const Ret = struct {
-        pub const base_tag = Tag.ret;
-        base: Inst,
-        args: struct {
-            operand: *Inst,
-        },
-    };
-
-    pub const RetVoid = struct {
-        pub const base_tag = Tag.retvoid;
-        base: Inst,
-        args: void,
-    };
-
-    pub const Sub = struct {
-        pub const base_tag = Tag.sub;
-        base: Inst,
-
-        args: struct {
-            lhs: *Inst,
-            rhs: *Inst,
-        },
-    };
+        val: Value,
 
-    pub const Unreach = struct {
-        pub const base_tag = Tag.unreach;
-        base: Inst,
-        args: void,
+        pub fn operandCount(self: *const Constant) usize {
+            return 0;
+        }
+        pub fn getOperand(self: *const Constant, index: usize) ?*Inst {
+            return null;
+        }
     };
 };
 
src-self-hosted/liveness.zig
@@ -25,53 +25,38 @@ fn analyzeWithTable(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst,
     while (i != 0) {
         i -= 1;
         const base = body.instructions[i];
-        try analyzeInstGeneric(arena, table, base);
+        try analyzeInst(arena, table, base);
     }
 }
 
-fn analyzeInstGeneric(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst, void), base: *ir.Inst) error{OutOfMemory}!void {
-    // Obtain the corresponding instruction type based on the tag type.
-    inline for (std.meta.declarations(ir.Inst)) |decl| {
-        switch (decl.data) {
-            .Type => |T| {
-                if (@typeInfo(T) == .Struct and @hasDecl(T, "base_tag")) {
-                    if (T.base_tag == base.tag) {
-                        return analyzeInst(arena, table, T, @fieldParentPtr(T, "base", base));
-                    }
-                }
-            },
-            else => {},
-        }
-    }
-    unreachable;
-}
-
-fn analyzeInst(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst, void), comptime T: type, inst: *T) error{OutOfMemory}!void {
-    if (table.contains(&inst.base)) {
-        inst.base.deaths = 0;
+fn analyzeInst(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst, void), base: *ir.Inst) error{OutOfMemory}!void {
+    if (table.contains(base)) {
+        base.deaths = 0;
     } else {
         // No tombstone for this instruction means it is never referenced,
         // and its birth marks its own death. Very metal ๐Ÿค˜
-        inst.base.deaths = 1 << ir.Inst.unreferenced_bit_index;
+        base.deaths = 1 << ir.Inst.unreferenced_bit_index;
     }
 
-    switch (T) {
-        ir.Inst.Constant => return,
-        ir.Inst.Block => {
-            try analyzeWithTable(arena, table, inst.args.body);
+    switch (base.tag) {
+        .constant => return,
+        .block => {
+            const inst = base.castTag(.block).?;
+            try analyzeWithTable(arena, table, inst.body);
             // We let this continue so that it can possibly mark the block as
             // unreferenced below.
         },
-        ir.Inst.CondBr => {
+        .condbr => {
+            const inst = base.castTag(.condbr).?;
             var true_table = std.AutoHashMap(*ir.Inst, void).init(table.allocator);
             defer true_table.deinit();
-            try true_table.ensureCapacity(inst.args.true_body.instructions.len);
-            try analyzeWithTable(arena, &true_table, inst.args.true_body);
+            try true_table.ensureCapacity(inst.then_body.instructions.len);
+            try analyzeWithTable(arena, &true_table, inst.then_body);
 
             var false_table = std.AutoHashMap(*ir.Inst, void).init(table.allocator);
             defer false_table.deinit();
-            try false_table.ensureCapacity(inst.args.false_body.instructions.len);
-            try analyzeWithTable(arena, &false_table, inst.args.false_body);
+            try false_table.ensureCapacity(inst.else_body.instructions.len);
+            try analyzeWithTable(arena, &false_table, inst.else_body);
 
             // Each death that occurs inside one branch, but not the other, needs
             // to be added as a death immediately upon entering the other branch.
@@ -112,47 +97,22 @@ fn analyzeInst(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst, void
             // instruction, and the deaths flag for the CondBr instruction will indicate whether the
             // condition's lifetime ends immediately before entering any branch.
         },
-        ir.Inst.Call => {
-            // Call instructions have a runtime-known number of operands so we have to handle them ourselves here.
-            const needed_bits = 1 + inst.args.args.len;
-            if (needed_bits <= ir.Inst.deaths_bits) {
-                var bit_i: ir.Inst.DeathsBitIndex = 0;
-                {
-                    const prev = try table.fetchPut(inst.args.func, {});
-                    if (prev == null) inst.base.deaths |= @as(ir.Inst.DeathsInt, 1) << bit_i;
-                    bit_i += 1;
-                }
-                for (inst.args.args) |arg| {
-                    const prev = try table.fetchPut(arg, {});
-                    if (prev == null) inst.base.deaths |= @as(ir.Inst.DeathsInt, 1) << bit_i;
-                    bit_i += 1;
-                }
-            } else {
-                @panic("Handle liveness analysis for function calls with many parameters");
-            }
-        },
         else => {},
     }
 
-    const Args = ir.Inst.Args(T);
-    if (Args == void) {
-        return;
-    }
-
-    comptime var arg_index: usize = 0;
-    inline for (std.meta.fields(Args)) |field| {
-        if (field.field_type == *ir.Inst) {
-            if (arg_index >= 6) {
-                @compileError("out of bits to mark deaths of operands");
-            }
-            const prev = try table.fetchPut(@field(inst.args, field.name), {});
+    const needed_bits = base.operandCount();
+    if (needed_bits <= ir.Inst.deaths_bits) {
+        var bit_i: ir.Inst.DeathsBitIndex = 0;
+        while (base.getOperand(bit_i)) |operand| : (bit_i += 1) {
+            const prev = try table.fetchPut(operand, {});
             if (prev == null) {
                 // Death.
-                inst.base.deaths |= 1 << arg_index;
+                base.deaths |= @as(ir.Inst.DeathsInt, 1) << bit_i;
             }
-            arg_index += 1;
         }
+    } else {
+        @panic("Handle liveness analysis for instructions with many parameters");
     }
 
-    std.log.debug(.liveness, "analyze {}: 0b{b}\n", .{ inst.base.tag, inst.base.deaths });
+    std.log.debug(.liveness, "analyze {}: 0b{b}\n", .{ base.tag, base.deaths });
 }
src-self-hosted/Module.zig
@@ -1349,8 +1349,8 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool {
 fn analyzeBodyValueAsType(self: *Module, block_scope: *Scope.Block, body: zir.Module.Body) !Type {
     try self.analyzeBody(&block_scope.base, body);
     for (block_scope.instructions.items) |inst| {
-        if (inst.cast(Inst.Ret)) |ret| {
-            const val = try self.resolveConstValue(&block_scope.base, ret.args.operand);
+        if (inst.castTag(.ret)) |ret| {
+            const val = try self.resolveConstValue(&block_scope.base, ret.operand);
             return val.toType();
         } else {
             return self.fail(&block_scope.base, inst.src, "unable to resolve comptime value", .{});
@@ -1938,16 +1938,132 @@ fn analyzeExport(self: *Module, scope: *Scope, src: usize, symbol_name: []const
     };
 }
 
-fn addNewInstArgs(
+fn addNoOp(
     self: *Module,
     block: *Scope.Block,
     src: usize,
     ty: Type,
-    comptime T: type,
-    args: Inst.Args(T),
+    comptime tag: Inst.Tag,
+) !*Inst {
+    const inst = try block.arena.create(tag.Type());
+    inst.* = .{
+        .base = .{
+            .tag = tag,
+            .ty = ty,
+            .src = src,
+        },
+    };
+    try block.instructions.append(self.gpa, &inst.base);
+    return &inst.base;
+}
+
+fn addUnOp(
+    self: *Module,
+    block: *Scope.Block,
+    src: usize,
+    ty: Type,
+    tag: Inst.Tag,
+    operand: *Inst,
+) !*Inst {
+    const inst = try block.arena.create(Inst.UnOp);
+    inst.* = .{
+        .base = .{
+            .tag = tag,
+            .ty = ty,
+            .src = src,
+        },
+        .operand = operand,
+    };
+    try block.instructions.append(self.gpa, &inst.base);
+    return &inst.base;
+}
+
+fn addBinOp(
+    self: *Module,
+    block: *Scope.Block,
+    src: usize,
+    ty: Type,
+    tag: Inst.Tag,
+    lhs: *Inst,
+    rhs: *Inst,
+) !*Inst {
+    const inst = try block.arena.create(Inst.BinOp);
+    inst.* = .{
+        .base = .{
+            .tag = tag,
+            .ty = ty,
+            .src = src,
+        },
+        .lhs = lhs,
+        .rhs = rhs,
+    };
+    try block.instructions.append(self.gpa, &inst.base);
+    return &inst.base;
+}
+
+fn addBr(
+    self: *Module,
+    scope_block: *Scope.Block,
+    src: usize,
+    target_block: *Inst.Block,
+    operand: *Inst,
+) !*Inst {
+    const inst = try scope_block.arena.create(Inst.Br);
+    inst.* = .{
+        .base = .{
+            .tag = .br,
+            .ty = Type.initTag(.noreturn),
+            .src = src,
+        },
+        .operand = operand,
+        .block = target_block,
+    };
+    try scope_block.instructions.append(self.gpa, &inst.base);
+    return &inst.base;
+}
+
+fn addCondBr(
+    self: *Module,
+    block: *Scope.Block,
+    src: usize,
+    condition: *Inst,
+    then_body: ir.Body,
+    else_body: ir.Body,
 ) !*Inst {
-    const inst = try self.addNewInst(block, src, ty, T);
-    inst.args = args;
+    const inst = try block.arena.create(Inst.CondBr);
+    inst.* = .{
+        .base = .{
+            .tag = .condbr,
+            .ty = Type.initTag(.noreturn),
+            .src = src,
+        },
+        .condition = condition,
+        .then_body = then_body,
+        .else_body = else_body,
+    };
+    try block.instructions.append(self.gpa, &inst.base);
+    return &inst.base;
+}
+
+fn addCall(
+    self: *Module,
+    block: *Scope.Block,
+    src: usize,
+    ty: Type,
+    func: *Inst,
+    args: []const *Inst,
+) !*Inst {
+    const inst = try block.arena.create(Inst.Call);
+    inst.* = .{
+        .base = .{
+            .tag = .call,
+            .ty = ty,
+            .src = src,
+        },
+        .func = func,
+        .args = args,
+    };
+    try block.instructions.append(self.gpa, &inst.base);
     return &inst.base;
 }
 
@@ -2017,7 +2133,6 @@ fn addNewInst(self: *Module, block: *Scope.Block, src: usize, ty: Type, comptime
             .ty = ty,
             .src = src,
         },
-        .args = undefined,
     };
     try block.instructions.append(self.gpa, &inst.base);
     return inst;
@@ -2269,7 +2384,7 @@ fn analyzeInstArg(self: *Module, scope: *Scope, inst: *zir.Inst.Arg) InnerError!
         });
     }
     const param_type = fn_ty.fnParamType(param_index);
-    return self.addNewInstArgs(b, inst.base.src, param_type, Inst.Arg, {});
+    return self.addNoOp(b, inst.base.src, param_type, .arg);
 }
 
 fn analyzeInstBlock(self: *Module, scope: *Scope, inst: *zir.Inst.Block) InnerError!*Inst {
@@ -2285,7 +2400,7 @@ fn analyzeInstBlock(self: *Module, scope: *Scope, inst: *zir.Inst.Block) InnerEr
             .ty = undefined, // Set after analysis.
             .src = inst.base.src,
         },
-        .args = undefined,
+        .body = undefined,
     };
 
     var child_block: Scope.Block = .{
@@ -2316,13 +2431,13 @@ fn analyzeInstBlock(self: *Module, scope: *Scope, inst: *zir.Inst.Block) InnerEr
     // to emit a jump instruction to after the block when it encounters the break.
     try parent_block.instructions.append(self.gpa, &block_inst.base);
     block_inst.base.ty = try self.resolvePeerTypes(scope, label.results.items);
-    block_inst.args.body = .{ .instructions = try parent_block.arena.dupe(*Inst, child_block.instructions.items) };
+    block_inst.body = .{ .instructions = try parent_block.arena.dupe(*Inst, child_block.instructions.items) };
     return &block_inst.base;
 }
 
 fn analyzeInstBreakpoint(self: *Module, scope: *Scope, inst: *zir.Inst.Breakpoint) InnerError!*Inst {
     const b = try self.requireRuntimeBlock(scope, inst.base.src);
-    return self.addNewInstArgs(b, inst.base.src, Type.initTag(.void), Inst.Breakpoint, {});
+    return self.addNoOp(b, inst.base.src, Type.initTag(.void), .breakpoint);
 }
 
 fn analyzeInstBreak(self: *Module, scope: *Scope, inst: *zir.Inst.Break) InnerError!*Inst {
@@ -2350,10 +2465,7 @@ fn analyzeBreak(
             if (label.zir_block == zir_block) {
                 try label.results.append(self.gpa, operand);
                 const b = try self.requireRuntimeBlock(scope, src);
-                return self.addNewInstArgs(b, src, Type.initTag(.noreturn), Inst.Br, .{
-                    .block = label.block_inst,
-                    .operand = operand,
-                });
+                return self.addBr(b, src, label.block_inst, operand);
             }
         }
         opt_block = block.parent;
@@ -2484,10 +2596,7 @@ fn analyzeInstCall(self: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerErro
     }
 
     const b = try self.requireRuntimeBlock(scope, inst.base.src);
-    return self.addNewInstArgs(b, inst.base.src, Type.initTag(.void), Inst.Call, .{
-        .func = func,
-        .args = casted_args,
-    });
+    return self.addCall(b, inst.base.src, Type.initTag(.void), func, casted_args);
 }
 
 fn analyzeInstFn(self: *Module, scope: *Scope, fn_inst: *zir.Inst.Fn) InnerError!*Inst {
@@ -2570,14 +2679,14 @@ fn analyzeInstAs(self: *Module, scope: *Scope, as: *zir.Inst.As) InnerError!*Ins
 }
 
 fn analyzeInstPtrToInt(self: *Module, scope: *Scope, ptrtoint: *zir.Inst.PtrToInt) InnerError!*Inst {
-    const ptr = try self.resolveInst(scope, ptrtoint.positionals.ptr);
+    const ptr = try self.resolveInst(scope, ptrtoint.positionals.operand);
     if (ptr.ty.zigTypeTag() != .Pointer) {
-        return self.fail(scope, ptrtoint.positionals.ptr.src, "expected pointer, found '{}'", .{ptr.ty});
+        return self.fail(scope, ptrtoint.positionals.operand.src, "expected pointer, found '{}'", .{ptr.ty});
     }
     // TODO handle known-pointer-address
     const b = try self.requireRuntimeBlock(scope, ptrtoint.base.src);
     const ty = Type.initTag(.usize);
-    return self.addNewInstArgs(b, ptrtoint.base.src, ty, Inst.PtrToInt, .{ .ptr = ptr });
+    return self.addUnOp(b, ptrtoint.base.src, ty, .ptrtoint, ptr);
 }
 
 fn analyzeInstFieldPtr(self: *Module, scope: *Scope, fieldptr: *zir.Inst.FieldPtr) InnerError!*Inst {
@@ -2734,10 +2843,7 @@ fn analyzeInstAdd(self: *Module, scope: *Scope, inst: *zir.Inst.Add) InnerError!
         }
 
         const b = try self.requireRuntimeBlock(scope, inst.base.src);
-        return self.addNewInstArgs(b, inst.base.src, lhs.ty, Inst.Add, .{
-            .lhs = lhs,
-            .rhs = rhs,
-        });
+        return self.addBinOp(b, inst.base.src, lhs.ty, .add, lhs, rhs);
     }
     return self.fail(scope, inst.base.src, "TODO analyze add for {} + {}", .{ lhs.ty.zigTypeTag(), rhs.ty.zigTypeTag() });
 }
@@ -2783,14 +2889,22 @@ fn analyzeInstAsm(self: *Module, scope: *Scope, assembly: *zir.Inst.Asm) InnerEr
     }
 
     const b = try self.requireRuntimeBlock(scope, assembly.base.src);
-    return self.addNewInstArgs(b, assembly.base.src, return_type, Inst.Assembly, .{
+    const inst = try b.arena.create(Inst.Assembly);
+    inst.* = .{
+        .base = .{
+            .tag = .assembly,
+            .ty = return_type,
+            .src = assembly.base.src,
+        },
         .asm_source = asm_source,
         .is_volatile = assembly.kw_args.@"volatile",
         .output = output,
         .inputs = inputs,
         .clobbers = clobbers,
         .args = args,
-    });
+    };
+    try b.instructions.append(self.gpa, &inst.base);
+    return &inst.base;
 }
 
 fn analyzeInstCmp(self: *Module, scope: *Scope, inst: *zir.Inst.Cmp) InnerError!*Inst {
@@ -2818,15 +2932,12 @@ fn analyzeInstCmp(self: *Module, scope: *Scope, inst: *zir.Inst.Cmp) InnerError!
             return self.constBool(scope, inst.base.src, if (op == .eq) is_null else !is_null);
         }
         const b = try self.requireRuntimeBlock(scope, inst.base.src);
-        switch (op) {
-            .eq => return self.addNewInstArgs(b, inst.base.src, Type.initTag(.bool), Inst.IsNull, .{
-                .operand = opt_operand,
-            }),
-            .neq => return self.addNewInstArgs(b, inst.base.src, Type.initTag(.bool), Inst.IsNonNull, .{
-                .operand = opt_operand,
-            }),
+        const inst_tag: Inst.Tag = switch (op) {
+            .eq => .isnull,
+            .neq => .isnonnull,
             else => unreachable,
-        }
+        };
+        return self.addUnOp(b, inst.base.src, Type.initTag(.bool), inst_tag, opt_operand);
     } else if (is_equality_cmp and
         ((lhs_ty_tag == .Null and rhs.ty.isCPtr()) or (rhs_ty_tag == .Null and lhs.ty.isCPtr())))
     {
@@ -2861,7 +2972,7 @@ fn analyzeInstBoolNot(self: *Module, scope: *Scope, inst: *zir.Inst.BoolNot) Inn
         return self.constBool(scope, inst.base.src, !val.toBool());
     }
     const b = try self.requireRuntimeBlock(scope, inst.base.src);
-    return self.addNewInstArgs(b, inst.base.src, bool_type, Inst.Not, .{ .operand = operand });
+    return self.addUnOp(b, inst.base.src, bool_type, .not, operand);
 }
 
 fn analyzeInstIsNull(self: *Module, scope: *Scope, inst: *zir.Inst.IsNull) InnerError!*Inst {
@@ -2879,7 +2990,7 @@ fn analyzeInstCondBr(self: *Module, scope: *Scope, inst: *zir.Inst.CondBr) Inner
     const cond = try self.coerce(scope, Type.initTag(.bool), uncasted_cond);
 
     if (try self.resolveDefinedValue(scope, cond)) |cond_val| {
-        const body = if (cond_val.toBool()) &inst.positionals.true_body else &inst.positionals.false_body;
+        const body = if (cond_val.toBool()) &inst.positionals.then_body else &inst.positionals.else_body;
         try self.analyzeBody(scope, body.*);
         return self.constVoid(scope, inst.base.src);
     }
@@ -2894,7 +3005,7 @@ fn analyzeInstCondBr(self: *Module, scope: *Scope, inst: *zir.Inst.CondBr) Inner
         .arena = parent_block.arena,
     };
     defer true_block.instructions.deinit(self.gpa);
-    try self.analyzeBody(&true_block.base, inst.positionals.true_body);
+    try self.analyzeBody(&true_block.base, inst.positionals.then_body);
 
     var false_block: Scope.Block = .{
         .parent = parent_block,
@@ -2904,13 +3015,11 @@ fn analyzeInstCondBr(self: *Module, scope: *Scope, inst: *zir.Inst.CondBr) Inner
         .arena = parent_block.arena,
     };
     defer false_block.instructions.deinit(self.gpa);
-    try self.analyzeBody(&false_block.base, inst.positionals.false_body);
+    try self.analyzeBody(&false_block.base, inst.positionals.else_body);
 
-    return self.addNewInstArgs(parent_block, inst.base.src, Type.initTag(.noreturn), Inst.CondBr, Inst.Args(Inst.CondBr){
-        .condition = cond,
-        .true_body = .{ .instructions = try scope.arena().dupe(*Inst, true_block.instructions.items) },
-        .false_body = .{ .instructions = try scope.arena().dupe(*Inst, false_block.instructions.items) },
-    });
+    const then_body: ir.Body = .{ .instructions = try scope.arena().dupe(*Inst, true_block.instructions.items) };
+    const else_body: ir.Body = .{ .instructions = try scope.arena().dupe(*Inst, false_block.instructions.items) };
+    return self.addCondBr(parent_block, inst.base.src, cond, then_body, else_body);
 }
 
 fn wantSafety(self: *Module, scope: *Scope) bool {
@@ -2926,20 +3035,20 @@ fn analyzeInstUnreachable(self: *Module, scope: *Scope, unreach: *zir.Inst.Unrea
     const b = try self.requireRuntimeBlock(scope, unreach.base.src);
     if (self.wantSafety(scope)) {
         // TODO Once we have a panic function to call, call it here instead of this.
-        _ = try self.addNewInstArgs(b, unreach.base.src, Type.initTag(.void), Inst.Breakpoint, {});
+        _ = try self.addNoOp(b, unreach.base.src, Type.initTag(.void), .breakpoint);
     }
-    return self.addNewInstArgs(b, unreach.base.src, Type.initTag(.noreturn), Inst.Unreach, {});
+    return self.addNoOp(b, unreach.base.src, Type.initTag(.noreturn), .unreach);
 }
 
 fn analyzeInstRet(self: *Module, scope: *Scope, inst: *zir.Inst.Return) InnerError!*Inst {
     const operand = try self.resolveInst(scope, inst.positionals.operand);
     const b = try self.requireRuntimeBlock(scope, inst.base.src);
-    return self.addNewInstArgs(b, inst.base.src, Type.initTag(.noreturn), Inst.Ret, .{ .operand = operand });
+    return self.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, operand);
 }
 
 fn analyzeInstRetVoid(self: *Module, scope: *Scope, inst: *zir.Inst.ReturnVoid) InnerError!*Inst {
     const b = try self.requireRuntimeBlock(scope, inst.base.src);
-    return self.addNewInstArgs(b, inst.base.src, Type.initTag(.noreturn), Inst.RetVoid, {});
+    return self.addNoOp(b, inst.base.src, Type.initTag(.noreturn), .retvoid);
 }
 
 fn analyzeBody(self: *Module, scope: *Scope, body: zir.Module.Body) !void {
@@ -3027,11 +3136,7 @@ fn cmpNumeric(
         };
         const casted_lhs = try self.coerce(scope, dest_type, lhs);
         const casted_rhs = try self.coerce(scope, dest_type, rhs);
-        return self.addNewInstArgs(b, src, dest_type, Inst.Cmp, .{
-            .lhs = casted_lhs,
-            .rhs = casted_rhs,
-            .op = op,
-        });
+        return self.addBinOp(b, src, dest_type, Inst.Tag.fromCmpOp(op), casted_lhs, casted_rhs);
     }
     // For mixed unsigned integer sizes, implicit cast both operands to the larger integer.
     // For mixed signed and unsigned integers, implicit cast both operands to a signed
@@ -3131,11 +3236,7 @@ fn cmpNumeric(
     const casted_lhs = try self.coerce(scope, dest_type, lhs);
     const casted_rhs = try self.coerce(scope, dest_type, rhs);
 
-    return self.addNewInstArgs(b, src, Type.initTag(.bool), Inst.Cmp, .{
-        .lhs = casted_lhs,
-        .rhs = casted_rhs,
-        .op = op,
-    });
+    return self.addBinOp(b, src, Type.initTag(.bool), Inst.Tag.fromCmpOp(op), casted_lhs, casted_rhs);
 }
 
 fn makeIntType(self: *Module, scope: *Scope, signed: bool, bits: u16) !Type {
@@ -3236,7 +3337,7 @@ fn bitcast(self: *Module, scope: *Scope, dest_type: Type, inst: *Inst) !*Inst {
     }
     // TODO validate the type size and other compile errors
     const b = try self.requireRuntimeBlock(scope, inst.src);
-    return self.addNewInstArgs(b, inst.src, dest_type, Inst.BitCast, .{ .operand = inst });
+    return self.addUnOp(b, inst.src, dest_type, .bitcast, inst);
 }
 
 fn coerceArrayPtrToSlice(self: *Module, scope: *Scope, dest_type: Type, inst: *Inst) !*Inst {
src-self-hosted/zir.zig
@@ -337,7 +337,7 @@ pub const Inst = struct {
         base: Inst,
 
         positionals: struct {
-            ptr: *Inst,
+            operand: *Inst,
         },
         kw_args: struct {},
     };
@@ -629,8 +629,8 @@ pub const Inst = struct {
 
         positionals: struct {
             condition: *Inst,
-            true_body: Module.Body,
-            false_body: Module.Body,
+            then_body: Module.Body,
+            else_body: Module.Body,
         },
         kw_args: struct {},
     };
@@ -1615,7 +1615,7 @@ const EmitZIR = struct {
         }
     }
 
-    fn emitTrivial(self: *EmitZIR, src: usize, comptime T: type) Allocator.Error!*Inst {
+    fn emitNoOp(self: *EmitZIR, src: usize, comptime T: type) Allocator.Error!*Inst {
         const new_inst = try self.arena.allocator.create(T);
         new_inst.* = .{
             .base = .{
@@ -1628,6 +1628,72 @@ const EmitZIR = struct {
         return &new_inst.base;
     }
 
+    fn emitCmp(
+        self: *EmitZIR,
+        src: usize,
+        new_body: ZirBody,
+        old_inst: *ir.Inst.BinOp,
+        op: std.math.CompareOperator,
+    ) Allocator.Error!*Inst {
+        const new_inst = try self.arena.allocator.create(Inst.Cmp);
+        new_inst.* = .{
+            .base = .{
+                .src = src,
+                .tag = Inst.Cmp.base_tag,
+            },
+            .positionals = .{
+                .lhs = try self.resolveInst(new_body, old_inst.lhs),
+                .rhs = try self.resolveInst(new_body, old_inst.rhs),
+                .op = op,
+            },
+            .kw_args = .{},
+        };
+        return &new_inst.base;
+    }
+
+    fn emitUnOp(
+        self: *EmitZIR,
+        src: usize,
+        new_body: ZirBody,
+        old_inst: *ir.Inst.UnOp,
+        comptime I: type,
+    ) Allocator.Error!*Inst {
+        const new_inst = try self.arena.allocator.create(I);
+        new_inst.* = .{
+            .base = .{
+                .src = src,
+                .tag = I.base_tag,
+            },
+            .positionals = .{
+                .operand = try self.resolveInst(new_body, old_inst.operand),
+            },
+            .kw_args = .{},
+        };
+        return &new_inst.base;
+    }
+
+    fn emitBinOp(
+        self: *EmitZIR,
+        src: usize,
+        new_body: ZirBody,
+        old_inst: *ir.Inst.BinOp,
+        comptime I: type,
+    ) Allocator.Error!*Inst {
+        const new_inst = try self.arena.allocator.create(I);
+        new_inst.* = .{
+            .base = .{
+                .src = src,
+                .tag = I.base_tag,
+            },
+            .positionals = .{
+                .lhs = try self.resolveInst(new_body, old_inst.lhs),
+                .rhs = try self.resolveInst(new_body, old_inst.rhs),
+            },
+            .kw_args = .{},
+        };
+        return &new_inst.base;
+    }
+
     fn emitBody(
         self: *EmitZIR,
         body: ir.Body,
@@ -1640,69 +1706,48 @@ const EmitZIR = struct {
         };
         for (body.instructions) |inst| {
             const new_inst = switch (inst.tag) {
-                .not => blk: {
-                    const old_inst = inst.cast(ir.Inst.Not).?;
-                    assert(inst.ty.zigTypeTag() == .Bool);
-                    const new_inst = try self.arena.allocator.create(Inst.BoolNot);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.BoolNot.base_tag,
-                        },
-                        .positionals = .{
-                            .operand = try self.resolveInst(new_body, old_inst.args.operand),
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
-                .add => blk: {
-                    const old_inst = inst.cast(ir.Inst.Add).?;
-                    const new_inst = try self.arena.allocator.create(Inst.Add);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.Add.base_tag,
-                        },
-                        .positionals = .{
-                            .lhs = try self.resolveInst(new_body, old_inst.args.lhs),
-                            .rhs = try self.resolveInst(new_body, old_inst.args.rhs),
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
-                .sub => blk: {
-                    const old_inst = inst.cast(ir.Inst.Sub).?;
-                    const new_inst = try self.arena.allocator.create(Inst.Sub);
+                .constant => unreachable, // excluded from function bodies
+
+                .arg => try self.emitNoOp(inst.src, Inst.Arg),
+                .breakpoint => try self.emitNoOp(inst.src, Inst.Breakpoint),
+                .unreach => try self.emitNoOp(inst.src, Inst.Unreachable),
+                .retvoid => try self.emitNoOp(inst.src, Inst.ReturnVoid),
+
+                .not => try self.emitUnOp(inst.src, new_body, inst.castTag(.not).?, Inst.BoolNot),
+                .ret => try self.emitUnOp(inst.src, new_body, inst.castTag(.ret).?, Inst.Return),
+                .ptrtoint => try self.emitUnOp(inst.src, new_body, inst.castTag(.ptrtoint).?, Inst.PtrToInt),
+                .isnull => try self.emitUnOp(inst.src, new_body, inst.castTag(.isnull).?, Inst.IsNull),
+                .isnonnull => try self.emitUnOp(inst.src, new_body, inst.castTag(.isnonnull).?, Inst.IsNonNull),
+
+                .add => try self.emitBinOp(inst.src, new_body, inst.castTag(.add).?, Inst.Add),
+                .sub => try self.emitBinOp(inst.src, new_body, inst.castTag(.sub).?, Inst.Sub),
+
+                .cmp_lt => try self.emitCmp(inst.src, new_body, inst.castTag(.cmp_lt).?, .lt),
+                .cmp_lte => try self.emitCmp(inst.src, new_body, inst.castTag(.cmp_lte).?, .lte),
+                .cmp_eq => try self.emitCmp(inst.src, new_body, inst.castTag(.cmp_eq).?, .eq),
+                .cmp_gte => try self.emitCmp(inst.src, new_body, inst.castTag(.cmp_gte).?, .gte),
+                .cmp_gt => try self.emitCmp(inst.src, new_body, inst.castTag(.cmp_gt).?, .gt),
+                .cmp_neq => try self.emitCmp(inst.src, new_body, inst.castTag(.cmp_neq).?, .neq),
+
+                .bitcast => blk: {
+                    const old_inst = inst.castTag(.bitcast).?;
+                    const new_inst = try self.arena.allocator.create(Inst.BitCast);
                     new_inst.* = .{
                         .base = .{
                             .src = inst.src,
-                            .tag = Inst.Sub.base_tag,
+                            .tag = Inst.BitCast.base_tag,
                         },
                         .positionals = .{
-                            .lhs = try self.resolveInst(new_body, old_inst.args.lhs),
-                            .rhs = try self.resolveInst(new_body, old_inst.args.rhs),
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
-                .arg => blk: {
-                    const old_inst = inst.cast(ir.Inst.Arg).?;
-                    const new_inst = try self.arena.allocator.create(Inst.Arg);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.Arg.base_tag,
+                            .dest_type = (try self.emitType(inst.src, inst.ty)).inst,
+                            .operand = try self.resolveInst(new_body, old_inst.operand),
                         },
-                        .positionals = .{},
                         .kw_args = .{},
                     };
                     break :blk &new_inst.base;
                 },
+
                 .block => blk: {
-                    const old_inst = inst.cast(ir.Inst.Block).?;
+                    const old_inst = inst.castTag(.block).?;
                     const new_inst = try self.arena.allocator.create(Inst.Block);
 
                     try self.block_table.put(old_inst, new_inst);
@@ -1710,7 +1755,7 @@ const EmitZIR = struct {
                     var block_body = std.ArrayList(*Inst).init(self.allocator);
                     defer block_body.deinit();
 
-                    try self.emitBody(old_inst.args.body, inst_table, &block_body);
+                    try self.emitBody(old_inst.body, inst_table, &block_body);
 
                     new_inst.* = .{
                         .base = .{
@@ -1725,47 +1770,49 @@ const EmitZIR = struct {
 
                     break :blk &new_inst.base;
                 },
-                .br => blk: {
-                    const old_inst = inst.cast(ir.Inst.Br).?;
-                    const new_block = self.block_table.get(old_inst.args.block).?;
-                    const new_inst = try self.arena.allocator.create(Inst.Break);
+
+                .brvoid => blk: {
+                    const old_inst = inst.cast(ir.Inst.BrVoid).?;
+                    const new_block = self.block_table.get(old_inst.block).?;
+                    const new_inst = try self.arena.allocator.create(Inst.BreakVoid);
                     new_inst.* = .{
                         .base = .{
                             .src = inst.src,
-                            .tag = Inst.Break.base_tag,
+                            .tag = Inst.BreakVoid.base_tag,
                         },
                         .positionals = .{
                             .block = new_block,
-                            .operand = try self.resolveInst(new_body, old_inst.args.operand),
                         },
                         .kw_args = .{},
                     };
                     break :blk &new_inst.base;
                 },
-                .breakpoint => try self.emitTrivial(inst.src, Inst.Breakpoint),
-                .brvoid => blk: {
-                    const old_inst = inst.cast(ir.Inst.BrVoid).?;
-                    const new_block = self.block_table.get(old_inst.args.block).?;
-                    const new_inst = try self.arena.allocator.create(Inst.BreakVoid);
+
+                .br => blk: {
+                    const old_inst = inst.castTag(.br).?;
+                    const new_block = self.block_table.get(old_inst.block).?;
+                    const new_inst = try self.arena.allocator.create(Inst.Break);
                     new_inst.* = .{
                         .base = .{
                             .src = inst.src,
-                            .tag = Inst.BreakVoid.base_tag,
+                            .tag = Inst.Break.base_tag,
                         },
                         .positionals = .{
                             .block = new_block,
+                            .operand = try self.resolveInst(new_body, old_inst.operand),
                         },
                         .kw_args = .{},
                     };
                     break :blk &new_inst.base;
                 },
+
                 .call => blk: {
-                    const old_inst = inst.cast(ir.Inst.Call).?;
+                    const old_inst = inst.castTag(.call).?;
                     const new_inst = try self.arena.allocator.create(Inst.Call);
 
-                    const args = try self.arena.allocator.alloc(*Inst, old_inst.args.args.len);
+                    const args = try self.arena.allocator.alloc(*Inst, old_inst.args.len);
                     for (args) |*elem, i| {
-                        elem.* = try self.resolveInst(new_body, old_inst.args.args[i]);
+                        elem.* = try self.resolveInst(new_body, old_inst.args[i]);
                     }
                     new_inst.* = .{
                         .base = .{
@@ -1773,48 +1820,31 @@ const EmitZIR = struct {
                             .tag = Inst.Call.base_tag,
                         },
                         .positionals = .{
-                            .func = try self.resolveInst(new_body, old_inst.args.func),
+                            .func = try self.resolveInst(new_body, old_inst.func),
                             .args = args,
                         },
                         .kw_args = .{},
                     };
                     break :blk &new_inst.base;
                 },
-                .unreach => try self.emitTrivial(inst.src, Inst.Unreachable),
-                .ret => blk: {
-                    const old_inst = inst.cast(ir.Inst.Ret).?;
-                    const new_inst = try self.arena.allocator.create(Inst.Return);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.Return.base_tag,
-                        },
-                        .positionals = .{
-                            .operand = try self.resolveInst(new_body, old_inst.args.operand),
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
-                .retvoid => try self.emitTrivial(inst.src, Inst.ReturnVoid),
-                .constant => unreachable, // excluded from function bodies
+
                 .assembly => blk: {
-                    const old_inst = inst.cast(ir.Inst.Assembly).?;
+                    const old_inst = inst.castTag(.assembly).?;
                     const new_inst = try self.arena.allocator.create(Inst.Asm);
 
-                    const inputs = try self.arena.allocator.alloc(*Inst, old_inst.args.inputs.len);
+                    const inputs = try self.arena.allocator.alloc(*Inst, old_inst.inputs.len);
                     for (inputs) |*elem, i| {
-                        elem.* = (try self.emitStringLiteral(inst.src, old_inst.args.inputs[i])).inst;
+                        elem.* = (try self.emitStringLiteral(inst.src, old_inst.inputs[i])).inst;
                     }
 
-                    const clobbers = try self.arena.allocator.alloc(*Inst, old_inst.args.clobbers.len);
+                    const clobbers = try self.arena.allocator.alloc(*Inst, old_inst.clobbers.len);
                     for (clobbers) |*elem, i| {
-                        elem.* = (try self.emitStringLiteral(inst.src, old_inst.args.clobbers[i])).inst;
+                        elem.* = (try self.emitStringLiteral(inst.src, old_inst.clobbers[i])).inst;
                     }
 
-                    const args = try self.arena.allocator.alloc(*Inst, old_inst.args.args.len);
+                    const args = try self.arena.allocator.alloc(*Inst, old_inst.args.len);
                     for (args) |*elem, i| {
-                        elem.* = try self.resolveInst(new_body, old_inst.args.args[i]);
+                        elem.* = try self.resolveInst(new_body, old_inst.args[i]);
                     }
 
                     new_inst.* = .{
@@ -1823,12 +1853,12 @@ const EmitZIR = struct {
                             .tag = Inst.Asm.base_tag,
                         },
                         .positionals = .{
-                            .asm_source = (try self.emitStringLiteral(inst.src, old_inst.args.asm_source)).inst,
+                            .asm_source = (try self.emitStringLiteral(inst.src, old_inst.asm_source)).inst,
                             .return_type = (try self.emitType(inst.src, inst.ty)).inst,
                         },
                         .kw_args = .{
-                            .@"volatile" = old_inst.args.is_volatile,
-                            .output = if (old_inst.args.output) |o|
+                            .@"volatile" = old_inst.is_volatile,
+                            .output = if (old_inst.output) |o|
                                 (try self.emitStringLiteral(inst.src, o)).inst
                             else
                                 null,
@@ -1839,65 +1869,18 @@ const EmitZIR = struct {
                     };
                     break :blk &new_inst.base;
                 },
-                .ptrtoint => blk: {
-                    const old_inst = inst.cast(ir.Inst.PtrToInt).?;
-                    const new_inst = try self.arena.allocator.create(Inst.PtrToInt);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.PtrToInt.base_tag,
-                        },
-                        .positionals = .{
-                            .ptr = try self.resolveInst(new_body, old_inst.args.ptr),
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
-                .bitcast => blk: {
-                    const old_inst = inst.cast(ir.Inst.BitCast).?;
-                    const new_inst = try self.arena.allocator.create(Inst.BitCast);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.BitCast.base_tag,
-                        },
-                        .positionals = .{
-                            .dest_type = (try self.emitType(inst.src, inst.ty)).inst,
-                            .operand = try self.resolveInst(new_body, old_inst.args.operand),
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
-                .cmp => blk: {
-                    const old_inst = inst.cast(ir.Inst.Cmp).?;
-                    const new_inst = try self.arena.allocator.create(Inst.Cmp);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.Cmp.base_tag,
-                        },
-                        .positionals = .{
-                            .lhs = try self.resolveInst(new_body, old_inst.args.lhs),
-                            .rhs = try self.resolveInst(new_body, old_inst.args.rhs),
-                            .op = old_inst.args.op,
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
+
                 .condbr => blk: {
-                    const old_inst = inst.cast(ir.Inst.CondBr).?;
+                    const old_inst = inst.castTag(.condbr).?;
 
-                    var true_body = std.ArrayList(*Inst).init(self.allocator);
-                    var false_body = std.ArrayList(*Inst).init(self.allocator);
+                    var then_body = std.ArrayList(*Inst).init(self.allocator);
+                    var else_body = std.ArrayList(*Inst).init(self.allocator);
 
-                    defer true_body.deinit();
-                    defer false_body.deinit();
+                    defer then_body.deinit();
+                    defer else_body.deinit();
 
-                    try self.emitBody(old_inst.args.true_body, inst_table, &true_body);
-                    try self.emitBody(old_inst.args.false_body, inst_table, &false_body);
+                    try self.emitBody(old_inst.then_body, inst_table, &then_body);
+                    try self.emitBody(old_inst.else_body, inst_table, &else_body);
 
                     const new_inst = try self.arena.allocator.create(Inst.CondBr);
                     new_inst.* = .{
@@ -1906,39 +1889,9 @@ const EmitZIR = struct {
                             .tag = Inst.CondBr.base_tag,
                         },
                         .positionals = .{
-                            .condition = try self.resolveInst(new_body, old_inst.args.condition),
-                            .true_body = .{ .instructions = true_body.toOwnedSlice() },
-                            .false_body = .{ .instructions = false_body.toOwnedSlice() },
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
-                .isnull => blk: {
-                    const old_inst = inst.cast(ir.Inst.IsNull).?;
-                    const new_inst = try self.arena.allocator.create(Inst.IsNull);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.IsNull.base_tag,
-                        },
-                        .positionals = .{
-                            .operand = try self.resolveInst(new_body, old_inst.args.operand),
-                        },
-                        .kw_args = .{},
-                    };
-                    break :blk &new_inst.base;
-                },
-                .isnonnull => blk: {
-                    const old_inst = inst.cast(ir.Inst.IsNonNull).?;
-                    const new_inst = try self.arena.allocator.create(Inst.IsNonNull);
-                    new_inst.* = .{
-                        .base = .{
-                            .src = inst.src,
-                            .tag = Inst.IsNonNull.base_tag,
-                        },
-                        .positionals = .{
-                            .operand = try self.resolveInst(new_body, old_inst.args.operand),
+                            .condition = try self.resolveInst(new_body, old_inst.condition),
+                            .then_body = .{ .instructions = then_body.toOwnedSlice() },
+                            .else_body = .{ .instructions = else_body.toOwnedSlice() },
                         },
                         .kw_args = .{},
                     };
test/stage2/compare_output.zig
@@ -267,5 +267,42 @@ pub fn addCases(ctx: *TestContext) !void {
         ,
             "",
         );
+
+        // Requires a second move. The register allocator should figure out to re-use rax.
+        case.addCompareOutput(
+            \\export fn _start() noreturn {
+            \\    add(3, 4);
+            \\
+            \\    exit();
+            \\}
+            \\
+            \\fn add(a: u32, b: u32) void {
+            \\    const c = a + b; // 7
+            \\    const d = a + c; // 10
+            \\    const e = d + b; // 14
+            \\    const f = d + e; // 24
+            \\    const g = e + f; // 38
+            \\    const h = f + g; // 62
+            \\    const i = g + h; // 100
+            \\    const j = i + d; // 110
+            \\    assert(j == 110);
+            \\}
+            \\
+            \\pub fn assert(ok: bool) void {
+            \\    if (!ok) unreachable; // assertion failure
+            \\}
+            \\
+            \\fn exit() noreturn {
+            \\    asm volatile ("syscall"
+            \\        :
+            \\        : [number] "{rax}" (231),
+            \\          [arg1] "{rdi}" (0)
+            \\        : "rcx", "r11", "memory"
+            \\    );
+            \\    unreachable;
+            \\}
+        ,
+            "",
+        );
     }
 }