diff --git a/src/arch/sparc64/CodeGen.zig b/src/arch/sparc64/CodeGen.zig index 7e9cdae414596f1d110177cdc2a7eee6ea511a76..d30bd651a2a13efbb17852c0f4a329c2af248c6d 100644 --- a/src/arch/sparc64/CodeGen.zig +++ b/src/arch/sparc64/CodeGen.zig @@ -123,6 +123,16 @@ const MCValue = union(enum) { immediate: u64, /// The value is in a target-specific register. register: Register, + /// The value is a tuple { wrapped, overflow } where + /// wrapped is stored in the register and the overflow bit is + /// stored in the C (signed) or V (unsigned) flag of the CCR. + /// + /// This MCValue is only generated by a add_with_overflow or + /// sub_with_overflow instruction operating on 32- or 64-bit values. + register_with_overflow: struct { + reg: Register, + flag: struct { cond: Instruction.ICondition, ccr: Instruction.CCR }, + }, /// The value is in memory at a hard-coded address. /// If the type is a pointer, it means the pointer address is at this memory location. memory: u64, @@ -131,12 +141,18 @@ const MCValue = union(enum) { stack_offset: u32, /// The value is a pointer to one of the stack variables (payload is stack offset). ptr_stack_offset: u32, - /// The value is in the compare flags assuming an unsigned operation, - /// with this operator applied on top of it. - compare_flags_unsigned: math.CompareOperator, - /// The value is in the compare flags assuming a signed operation, - /// with this operator applied on top of it. - compare_flags_signed: math.CompareOperator, + /// The value is in the specified CCR assuming an unsigned operation, + /// with the operator applied on top of it. + compare_flags_unsigned: struct { + cmp: math.CompareOperator, + ccr: Instruction.CCR, + }, + /// The value is in the specified CCR assuming an signed operation, + /// with the operator applied on top of it. + compare_flags_signed: struct { + cmp: math.CompareOperator, + ccr: Instruction.CCR, + }, fn isMemory(mcv: MCValue) bool { return switch (mcv) { @@ -501,7 +517,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void { .shl_sat => @panic("TODO try self.airShlSat(inst)"), .min => @panic("TODO try self.airMin(inst)"), .max => @panic("TODO try self.airMax(inst)"), - .slice => @panic("TODO try self.airSlice(inst)"), + .slice => try self.airSlice(inst), .sqrt, .sin, @@ -519,8 +535,8 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void { .trunc_float, => @panic("TODO try self.airUnaryMath(inst)"), - .add_with_overflow => @panic("TODO try self.airAddWithOverflow(inst)"), - .sub_with_overflow => @panic("TODO try self.airSubWithOverflow(inst)"), + .add_with_overflow => try self.airAddSubWithOverflow(inst), + .sub_with_overflow => try self.airAddSubWithOverflow(inst), .mul_with_overflow => @panic("TODO try self.airMulWithOverflow(inst)"), .shl_with_overflow => @panic("TODO try self.airShlWithOverflow(inst)"), @@ -570,14 +586,14 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void { .is_err_ptr => @panic("TODO try self.airIsErrPtr(inst)"), .load => try self.airLoad(inst), .loop => try self.airLoop(inst), - .not => @panic("TODO try self.airNot(inst)"), + .not => try self.airNot(inst), .ptrtoint => @panic("TODO try self.airPtrToInt(inst)"), .ret => try self.airRet(inst), .ret_load => try self.airRetLoad(inst), .store => try self.airStore(inst), .struct_field_ptr=> @panic("TODO try self.airStructFieldPtr(inst)"), - .struct_field_val=> @panic("TODO try self.airStructFieldVal(inst)"), - .array_to_slice => @panic("TODO try self.airArrayToSlice(inst)"), + .struct_field_val=> try self.airStructFieldVal(inst), + .array_to_slice => try self.airArrayToSlice(inst), .int_to_float => @panic("TODO try self.airIntToFloat(inst)"), .float_to_int => @panic("TODO try self.airFloatToInt(inst)"), .cmpxchg_strong => @panic("TODO try self.airCmpxchg(inst)"), @@ -647,7 +663,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void { .slice_elem_val => try self.airSliceElemVal(inst), .slice_elem_ptr => @panic("TODO try self.airSliceElemPtr(inst)"), .ptr_elem_val => @panic("TODO try self.airPtrElemVal(inst)"), - .ptr_elem_ptr => @panic("TODO try self.airPtrElemPtr(inst)"), + .ptr_elem_ptr => try self.airPtrElemPtr(inst), .constant => unreachable, // excluded from function bodies .const_ty => unreachable, // excluded from function bodies @@ -666,13 +682,15 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void { .wrap_optional => @panic("TODO try self.airWrapOptional(inst)"), .wrap_errunion_payload => @panic("TODO try self.airWrapErrUnionPayload(inst)"), - .wrap_errunion_err => @panic("TODO try self.airWrapErrUnionErr(inst)"), + .wrap_errunion_err => try self.airWrapErrUnionErr(inst), .wasm_memory_size => unreachable, .wasm_memory_grow => unreachable, // zig fmt: on } + assert(!self.register_manager.lockedRegsExist()); + if (std.debug.runtime_safety) { if (self.air_bookkeeping < old_air_bookkeeping + 1) { std.debug.panic("in codegen.zig, handling of AIR instruction %{d} ('{}') did not do proper bookkeeping. Look for a missing call to finishAir.", .{ inst, air_tags[inst] }); @@ -681,11 +699,112 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void { } } +fn airAddSubWithOverflow(self: *Self, inst: Air.Inst.Index) !void { + const tag = self.air.instructions.items(.tag)[inst]; + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const extra = self.air.extraData(Air.Bin, ty_pl.payload).data; + const result: MCValue = if (self.liveness.isUnused(inst)) .dead else result: { + const lhs = try self.resolveInst(extra.lhs); + const rhs = try self.resolveInst(extra.rhs); + const lhs_ty = self.air.typeOf(extra.lhs); + const rhs_ty = self.air.typeOf(extra.rhs); + + switch (lhs_ty.zigTypeTag()) { + .Vector => return self.fail("TODO implement add_with_overflow/sub_with_overflow for vectors", .{}), + .Int => { + const mod = self.bin_file.options.module.?; + assert(lhs_ty.eql(rhs_ty, mod)); + const int_info = lhs_ty.intInfo(self.target.*); + switch (int_info.bits) { + 32, 64 => { + // Only say yes if the operation is + // commutative, i.e. we can swap both of the + // operands + const lhs_immediate_ok = switch (tag) { + .add_with_overflow => lhs == .immediate and lhs.immediate <= std.math.maxInt(u12), + .sub_with_overflow => false, + else => unreachable, + }; + const rhs_immediate_ok = switch (tag) { + .add_with_overflow, + .sub_with_overflow, + => rhs == .immediate and rhs.immediate <= std.math.maxInt(u12), + else => unreachable, + }; + + const mir_tag: Mir.Inst.Tag = switch (tag) { + .add_with_overflow => .addcc, + .sub_with_overflow => .subcc, + else => unreachable, + }; + + try self.spillCompareFlagsIfOccupied(); + self.compare_flags_inst = inst; + + const dest = blk: { + if (rhs_immediate_ok) { + break :blk try self.binOpImmediate(mir_tag, lhs, rhs, lhs_ty, false, null); + } else if (lhs_immediate_ok) { + // swap lhs and rhs + break :blk try self.binOpImmediate(mir_tag, rhs, lhs, rhs_ty, true, null); + } else { + break :blk try self.binOpRegister(mir_tag, lhs, rhs, lhs_ty, rhs_ty, null); + } + }; + + const cond = switch (int_info.signedness) { + .unsigned => switch (tag) { + .add_with_overflow => Instruction.ICondition.cs, + .sub_with_overflow => Instruction.ICondition.cc, + else => unreachable, + }, + .signed => Instruction.ICondition.vs, + }; + + const ccr = switch (int_info.bits) { + 32 => Instruction.CCR.icc, + 64 => Instruction.CCR.xcc, + else => unreachable, + }; + + break :result MCValue{ .register_with_overflow = .{ + .reg = dest.register, + .flag = .{ .cond = cond, .ccr = ccr }, + } }; + }, + else => return self.fail("TODO overflow operations on other integer sizes", .{}), + } + }, + else => unreachable, + } + }; + return self.finishAir(inst, result, .{ extra.lhs, extra.rhs, .none }); +} + fn airAlloc(self: *Self, inst: Air.Inst.Index) !void { const stack_offset = try self.allocMemPtr(inst); return self.finishAir(inst, .{ .ptr_stack_offset = stack_offset }, .{ .none, .none, .none }); } +fn airArrayToSlice(self: *Self, inst: Air.Inst.Index) !void { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const result: MCValue = if (self.liveness.isUnused(inst)) .dead else result: { + const ptr_ty = self.air.typeOf(ty_op.operand); + const ptr = try self.resolveInst(ty_op.operand); + const array_ty = ptr_ty.childType(); + const array_len = @intCast(u32, array_ty.arrayLen()); + + const ptr_bits = self.target.cpu.arch.ptrBitWidth(); + const ptr_bytes = @divExact(ptr_bits, 8); + + const stack_offset = try self.allocMem(inst, ptr_bytes * 2, ptr_bytes * 2); + try self.genSetStack(ptr_ty, stack_offset, ptr); + try self.genSetStack(Type.initTag(.usize), stack_offset - ptr_bytes, .{ .immediate = array_len }); + break :result MCValue{ .stack_offset = stack_offset }; + }; + return self.finishAir(inst, result, .{ ty_op.operand, .none, .none }); +} + fn airAsm(self: *Self, inst: Air.Inst.Index) !void { const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; const extra = self.air.extraData(Air.Asm, ty_pl.payload); @@ -896,9 +1015,14 @@ fn airBlock(self: *Self, inst: Air.Inst.Index) !void { const relocs = &self.blocks.getPtr(inst).?.relocs; if (relocs.items.len > 0 and relocs.items[relocs.items.len - 1] == self.mir_instructions.len - 1) { // If the last Mir instruction is the last relocation (which - // would just jump one instruction further), it can be safely + // would just jump two instruction further), it can be safely // removed - self.mir_instructions.orderedRemove(relocs.pop()); + const index = relocs.pop(); + + // First, remove the delay slot, then remove + // the branch instruction itself. + self.mir_instructions.orderedRemove(index + 1); + self.mir_instructions.orderedRemove(index); } for (relocs.items) |reloc| { try self.performReloc(reloc); @@ -945,6 +1069,11 @@ fn airCall(self: *Self, inst: Air.Inst.Index, modifier: std.builtin.CallOptions. var info = try self.resolveCallingConventionValues(fn_ty, .caller); defer info.deinit(self); + + // CCR is volatile across function calls + // (SCD 2.4.1, page 3P-10) + try self.spillCompareFlagsIfOccupied(); + for (info.args) |mc_arg, arg_i| { const arg = args[arg_i]; const arg_ty = self.air.typeOf(arg); @@ -952,13 +1081,6 @@ fn airCall(self: *Self, inst: Air.Inst.Index, modifier: std.builtin.CallOptions. switch (mc_arg) { .none => continue, - .undef => unreachable, - .immediate => unreachable, - .unreach => unreachable, - .dead => unreachable, - .memory => unreachable, - .compare_flags_signed => unreachable, - .compare_flags_unsigned => unreachable, .register => |reg| { try self.register_manager.getReg(reg, null); try self.genSetReg(arg_ty, reg, arg_mcv); @@ -969,6 +1091,7 @@ fn airCall(self: *Self, inst: Air.Inst.Index, modifier: std.builtin.CallOptions. .ptr_stack_offset => { return self.fail("TODO implement calling with MCValue.ptr_stack_offset arg", .{}); }, + else => unreachable, } } @@ -1089,8 +1212,8 @@ fn airCmp(self: *Self, inst: Air.Inst.Index, op: math.CompareOperator) !void { self.compare_flags_inst = inst; break :result switch (int_info.signedness) { - .signed => MCValue{ .compare_flags_signed = op }, - .unsigned => MCValue{ .compare_flags_unsigned = op }, + .signed => MCValue{ .compare_flags_signed = .{ .cmp = op, .ccr = .xcc } }, + .unsigned => MCValue{ .compare_flags_unsigned = .{ .cmp = op, .ccr = .xcc } }, }; } else { return self.fail("TODO SPARCv9 cmp for ints > 64 bits", .{}); @@ -1116,16 +1239,20 @@ fn airCondBr(self: *Self, inst: Air.Inst.Index) !void { .tag = .bpcc, .data = .{ .branch_predict_int = .{ - .ccr = .xcc, + .ccr = switch (cond) { + .compare_flags_signed => |cmp_op| cmp_op.ccr, + .compare_flags_unsigned => |cmp_op| cmp_op.ccr, + else => unreachable, + }, .cond = switch (cond) { .compare_flags_signed => |cmp_op| blk: { // Here we map to the opposite condition because the jump is to the false branch. - const condition = Instruction.ICondition.fromCompareOperatorSigned(cmp_op); + const condition = Instruction.ICondition.fromCompareOperatorSigned(cmp_op.cmp); break :blk condition.negate(); }, .compare_flags_unsigned => |cmp_op| blk: { // Here we map to the opposite condition because the jump is to the false branch. - const condition = Instruction.ICondition.fromCompareOperatorUnsigned(cmp_op); + const condition = Instruction.ICondition.fromCompareOperatorUnsigned(cmp_op.cmp); break :blk condition.negate(); }, else => unreachable, @@ -1402,6 +1529,133 @@ fn airLoop(self: *Self, inst: Air.Inst.Index) !void { return self.finishAirBookkeeping(); } +fn airNot(self: *Self, inst: Air.Inst.Index) !void { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const result: MCValue = if (self.liveness.isUnused(inst)) .dead else result: { + const operand = try self.resolveInst(ty_op.operand); + const operand_ty = self.air.typeOf(ty_op.operand); + switch (operand) { + .dead => unreachable, + .unreach => unreachable, + .compare_flags_unsigned => |op| { + const r = MCValue{ + .compare_flags_unsigned = .{ + .cmp = switch (op.cmp) { + .gte => .lt, + .gt => .lte, + .neq => .eq, + .lt => .gte, + .lte => .gt, + .eq => .neq, + }, + .ccr = op.ccr, + }, + }; + break :result r; + }, + .compare_flags_signed => |op| { + const r = MCValue{ + .compare_flags_signed = .{ + .cmp = switch (op.cmp) { + .gte => .lt, + .gt => .lte, + .neq => .eq, + .lt => .gte, + .lte => .gt, + .eq => .neq, + }, + .ccr = op.ccr, + }, + }; + break :result r; + }, + else => { + switch (operand_ty.zigTypeTag()) { + .Bool => { + // TODO convert this to mvn + and + const op_reg = switch (operand) { + .register => |r| r, + else => try self.copyToTmpRegister(operand_ty, operand), + }; + const reg_lock = self.register_manager.lockRegAssumeUnused(op_reg); + defer self.register_manager.unlockReg(reg_lock); + + const dest_reg = blk: { + if (operand == .register and self.reuseOperand(inst, ty_op.operand, 0, operand)) { + break :blk op_reg; + } + + const reg = try self.register_manager.allocReg(null, gp); + break :blk reg; + }; + + _ = try self.addInst(.{ + .tag = .xor, + .data = .{ + .arithmetic_3op = .{ + .is_imm = true, + .rd = dest_reg, + .rs1 = op_reg, + .rs2_or_imm = .{ .imm = 1 }, + }, + }, + }); + + break :result MCValue{ .register = dest_reg }; + }, + .Vector => return self.fail("TODO bitwise not for vectors", .{}), + .Int => { + const int_info = operand_ty.intInfo(self.target.*); + if (int_info.bits <= 64) { + const op_reg = switch (operand) { + .register => |r| r, + else => try self.copyToTmpRegister(operand_ty, operand), + }; + const reg_lock = self.register_manager.lockRegAssumeUnused(op_reg); + defer self.register_manager.unlockReg(reg_lock); + + const dest_reg = blk: { + if (operand == .register and self.reuseOperand(inst, ty_op.operand, 0, operand)) { + break :blk op_reg; + } + + const reg = try self.register_manager.allocReg(null, gp); + break :blk reg; + }; + + _ = try self.addInst(.{ + .tag = .not, + .data = .{ + .arithmetic_2op = .{ + .is_imm = false, + .rs1 = dest_reg, + .rs2_or_imm = .{ .rs2 = op_reg }, + }, + }, + }); + + try self.truncRegister(dest_reg, dest_reg, int_info.signedness, int_info.bits); + + break :result MCValue{ .register = dest_reg }; + } else { + return self.fail("TODO AArch64 not on integers > u64/i64", .{}); + } + }, + else => unreachable, + } + }, + } + }; + return self.finishAir(inst, result, .{ ty_op.operand, .none, .none }); +} + +fn airPtrElemPtr(self: *Self, inst: Air.Inst.Index) !void { + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const extra = self.air.extraData(Air.Bin, ty_pl.payload).data; + const result: MCValue = if (self.liveness.isUnused(inst)) .dead else return self.fail("TODO implement ptr_elem_ptr for {}", .{self.target.cpu.arch}); + return self.finishAir(inst, result, .{ extra.lhs, extra.rhs, .none }); +} + fn airRet(self: *Self, inst: Air.Inst.Index) !void { const un_op = self.air.instructions.items(.data)[inst].un_op; const operand = try self.resolveInst(un_op); @@ -1422,6 +1676,26 @@ fn airRetPtr(self: *Self, inst: Air.Inst.Index) !void { return self.finishAir(inst, .{ .ptr_stack_offset = stack_offset }, .{ .none, .none, .none }); } +fn airSlice(self: *Self, inst: Air.Inst.Index) !void { + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const bin_op = self.air.extraData(Air.Bin, ty_pl.payload).data; + const result: MCValue = if (self.liveness.isUnused(inst)) .dead else result: { + const ptr = try self.resolveInst(bin_op.lhs); + const ptr_ty = self.air.typeOf(bin_op.lhs); + const len = try self.resolveInst(bin_op.rhs); + const len_ty = self.air.typeOf(bin_op.rhs); + + const ptr_bits = self.target.cpu.arch.ptrBitWidth(); + const ptr_bytes = @divExact(ptr_bits, 8); + + const stack_offset = try self.allocMem(inst, ptr_bytes * 2, ptr_bytes * 2); + try self.genSetStack(ptr_ty, stack_offset, ptr); + try self.genSetStack(len_ty, stack_offset - ptr_bytes, len); + break :result MCValue{ .stack_offset = stack_offset }; + }; + return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none }); +} + fn airSliceElemVal(self: *Self, inst: Air.Inst.Index) !void { const is_volatile = false; // TODO const bin_op = self.air.instructions.items(.data)[inst].bin_op; @@ -1505,6 +1779,75 @@ fn airStructFieldPtrIndex(self: *Self, inst: Air.Inst.Index, index: u8) !void { return self.finishAir(inst, result, .{ ty_op.operand, .none, .none }); } +fn airStructFieldVal(self: *Self, inst: Air.Inst.Index) !void { + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const extra = self.air.extraData(Air.StructField, ty_pl.payload).data; + const operand = extra.struct_operand; + const index = extra.field_index; + const result: MCValue = if (self.liveness.isUnused(inst)) .dead else result: { + const mcv = try self.resolveInst(operand); + const struct_ty = self.air.typeOf(operand); + const struct_field_offset = @intCast(u32, struct_ty.structFieldOffset(index, self.target.*)); + + switch (mcv) { + .dead, .unreach => unreachable, + .stack_offset => |off| { + break :result MCValue{ .stack_offset = off - struct_field_offset }; + }, + .memory => |addr| { + break :result MCValue{ .memory = addr + struct_field_offset }; + }, + .register_with_overflow => |rwo| { + switch (index) { + 0 => { + // get wrapped value: return register + break :result MCValue{ .register = rwo.reg }; + }, + 1 => { + // TODO return special MCValue condition flags + // get overflow bit: set register to C flag + // resp. V flag + const dest_reg = try self.register_manager.allocReg(null, gp); + + // TODO handle floating point CCRs + assert(rwo.flag.ccr == .xcc or rwo.flag.ccr == .icc); + + _ = try self.addInst(.{ + .tag = .mov, + .data = .{ + .arithmetic_2op = .{ + .is_imm = false, + .rs1 = dest_reg, + .rs2_or_imm = .{ .rs2 = .g0 }, + }, + }, + }); + + _ = try self.addInst(.{ + .tag = .movcc, + .data = .{ + .conditional_move = .{ + .ccr = rwo.flag.ccr, + .cond = .{ .icond = rwo.flag.cond }, + .is_imm = true, + .rd = dest_reg, + .rs2_or_imm = .{ .imm = 1 }, + }, + }, + }); + + break :result MCValue{ .register = dest_reg }; + }, + else => unreachable, + } + }, + else => return self.fail("TODO implement codegen struct_field_val for {}", .{mcv}), + } + }; + + return self.finishAir(inst, result, .{ extra.struct_operand, .none, .none }); +} + fn airSwitch(self: *Self, inst: Air.Inst.Index) !void { _ = self; _ = inst; @@ -1537,6 +1880,20 @@ fn airUnwrapErrPayload(self: *Self, inst: Air.Inst.Index) !void { return self.finishAir(inst, result, .{ ty_op.operand, .none, .none }); } +/// E to E!T +fn airWrapErrUnionErr(self: *Self, inst: Air.Inst.Index) !void { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const result: MCValue = if (self.liveness.isUnused(inst)) .dead else result: { + const error_union_ty = self.air.getRefType(ty_op.ty); + const payload_ty = error_union_ty.errorUnionPayload(); + const mcv = try self.resolveInst(ty_op.operand); + if (!payload_ty.hasRuntimeBits()) break :result mcv; + + return self.fail("TODO implement wrap errunion error for non-empty payloads", .{}); + }; + return self.finishAir(inst, result, .{ ty_op.operand, .none, .none }); +} + // Common helper functions /// Adds a Type to the .debug_info at the current position. The bytes will be populated later, @@ -1571,8 +1928,8 @@ fn allocMem(self: *Self, inst: Air.Inst.Index, abi_size: u32, abi_align: u32) !u if (abi_align > self.stack_align) self.stack_align = abi_align; // TODO find a free slot instead of always appending - const offset = mem.alignForwardGeneric(u32, self.next_stack_offset, abi_align); - self.next_stack_offset = offset + abi_size; + const offset = mem.alignForwardGeneric(u32, self.next_stack_offset, abi_align) + abi_size; + self.next_stack_offset = offset; if (self.next_stack_offset > self.max_end_stack) self.max_end_stack = self.next_stack_offset; try self.stack.putNoClobber(self.gpa, offset, .{ @@ -1708,21 +2065,34 @@ fn binOp( assert(lhs_ty.eql(rhs_ty, mod)); const int_info = lhs_ty.intInfo(self.target.*); if (int_info.bits <= 64) { - // If LHS is immediate, then swap it with RHS. - const lhs_is_imm = lhs == .immediate; - const new_lhs = if (lhs_is_imm) rhs else lhs; - const new_rhs = if (lhs_is_imm) lhs else rhs; - const new_lhs_ty = if (lhs_is_imm) rhs_ty else lhs_ty; - const new_rhs_ty = if (lhs_is_imm) lhs_ty else rhs_ty; + // Only say yes if the operation is + // commutative, i.e. we can swap both of the + // operands + const lhs_immediate_ok = switch (tag) { + .mul => lhs == .immediate and lhs.immediate <= std.math.maxInt(u12), + else => unreachable, + }; + const rhs_immediate_ok = switch (tag) { + .mul => rhs == .immediate and rhs.immediate <= std.math.maxInt(u12), + else => unreachable, + }; - // At this point, RHS might be an immediate - // If it's a power of two immediate then we emit an shl instead - // TODO add similar checks for LHS - if (new_rhs == .immediate and math.isPowerOfTwo(new_rhs.immediate)) { - return try self.binOp(.shl, new_lhs, .{ .immediate = math.log2(new_rhs.immediate) }, new_lhs_ty, Type.usize, metadata); + const mir_tag: Mir.Inst.Tag = switch (tag) { + .mul => .mulx, + else => unreachable, + }; + + if (rhs_immediate_ok) { + // At this point, rhs is an immediate + return try self.binOpImmediate(mir_tag, lhs, rhs, lhs_ty, false, metadata); + } else if (lhs_immediate_ok) { + // swap lhs and rhs + // At this point, lhs is an immediate + return try self.binOpImmediate(mir_tag, rhs, lhs, rhs_ty, true, metadata); + } else { + // TODO convert large immediates to register before adding + return try self.binOpRegister(mir_tag, lhs, rhs, lhs_ty, rhs_ty, metadata); } - - return try self.binOpRegister(.mulx, new_lhs, new_rhs, new_lhs_ty, new_rhs_ty, metadata); } else { return self.fail("TODO binary operations on int with bits > 64", .{}); } @@ -1867,6 +2237,7 @@ fn binOpImmediate( defer if (new_lhs_lock) |reg| self.register_manager.unlockReg(reg); const dest_reg = switch (mir_tag) { + .cmp => undefined, // cmp has no destination register else => if (metadata) |md| blk: { if (lhs_is_register and self.reuseOperand( md.inst, @@ -1887,6 +2258,7 @@ fn binOpImmediate( const mir_data: Mir.Inst.Data = switch (mir_tag) { .add, + .addcc, .mulx, .subcc, => .{ @@ -1985,6 +2357,7 @@ fn binOpRegister( defer if (new_rhs_lock) |reg| self.register_manager.unlockReg(reg); const dest_reg = switch (mir_tag) { + .cmp => undefined, // cmp has no destination register else => if (metadata) |md| blk: { if (lhs_is_register and self.reuseOperand(md.inst, md.lhs, 0, lhs)) { break :blk lhs_reg; @@ -2003,6 +2376,7 @@ fn binOpRegister( const mir_data: Mir.Inst.Data = switch (mir_tag) { .add, + .addcc, .mulx, .subcc, => .{ @@ -2293,8 +2667,47 @@ fn genSetReg(self: *Self, ty: Type, reg: Register, mcv: MCValue) InnerError!void switch (mcv) { .dead => unreachable, .unreach, .none => return, // Nothing to do. - .compare_flags_signed => return self.fail("TODO: genSetReg for compare_flags_signed", .{}), - .compare_flags_unsigned => return self.fail("TODO: genSetReg for compare_flags_unsigned", .{}), + .compare_flags_signed, + .compare_flags_unsigned, + => { + const condition = switch (mcv) { + .compare_flags_unsigned => |op| Instruction.ICondition.fromCompareOperatorUnsigned(op.cmp), + .compare_flags_signed => |op| Instruction.ICondition.fromCompareOperatorSigned(op.cmp), + else => unreachable, + }; + + const ccr = switch (mcv) { + .compare_flags_unsigned => |op| op.ccr, + .compare_flags_signed => |op| op.ccr, + else => unreachable, + }; + // TODO handle floating point CCRs + assert(ccr == .xcc or ccr == .icc); + + _ = try self.addInst(.{ + .tag = .mov, + .data = .{ + .arithmetic_2op = .{ + .is_imm = false, + .rs1 = reg, + .rs2_or_imm = .{ .rs2 = .g0 }, + }, + }, + }); + + _ = try self.addInst(.{ + .tag = .movcc, + .data = .{ + .conditional_move = .{ + .ccr = ccr, + .cond = .{ .icond = condition }, + .is_imm = true, + .rd = reg, + .rs2_or_imm = .{ .imm = 1 }, + }, + }, + }); + }, .undef => { if (!self.wantSafety()) return; // The already existing value will do just fine. @@ -2302,8 +2715,9 @@ fn genSetReg(self: *Self, ty: Type, reg: Register, mcv: MCValue) InnerError!void return self.genSetReg(ty, reg, .{ .immediate = 0xaaaaaaaaaaaaaaaa }); }, .ptr_stack_offset => |off| { - const simm13 = math.cast(u12, off + abi.stack_bias + abi.stack_reserved_area) orelse - return self.fail("TODO larger stack offsets", .{}); + const real_offset = off + abi.stack_bias + abi.stack_reserved_area; + const simm13 = math.cast(i13, real_offset) orelse + return self.fail("TODO larger stack offsets: {}", .{real_offset}); _ = try self.addInst(.{ .tag = .add, @@ -2427,6 +2841,7 @@ fn genSetReg(self: *Self, ty: Type, reg: Register, mcv: MCValue) InnerError!void }, }); }, + .register_with_overflow => unreachable, .memory => |addr| { // The value is in memory at a hard-coded address. // If the type is a pointer, it means the pointer address is at this memory location. @@ -2436,7 +2851,7 @@ fn genSetReg(self: *Self, ty: Type, reg: Register, mcv: MCValue) InnerError!void .stack_offset => |off| { const real_offset = off + abi.stack_bias + abi.stack_reserved_area; const simm13 = math.cast(i13, real_offset) orelse - return self.fail("TODO larger stack offsets", .{}); + return self.fail("TODO larger stack offsets: {}", .{real_offset}); try self.genLoad(reg, .sp, i13, simm13, ty.abiSize(self.target.*)); }, } @@ -2470,9 +2885,50 @@ fn genSetStack(self: *Self, ty: Type, stack_offset: u32, mcv: MCValue) InnerErro .register => |reg| { const real_offset = stack_offset + abi.stack_bias + abi.stack_reserved_area; const simm13 = math.cast(i13, real_offset) orelse - return self.fail("TODO larger stack offsets", .{}); + return self.fail("TODO larger stack offsets: {}", .{real_offset}); return self.genStore(reg, .sp, i13, simm13, abi_size); }, + .register_with_overflow => |rwo| { + const reg_lock = self.register_manager.lockReg(rwo.reg); + defer if (reg_lock) |locked_reg| self.register_manager.unlockReg(locked_reg); + + const wrapped_ty = ty.structFieldType(0); + try self.genSetStack(wrapped_ty, stack_offset, .{ .register = rwo.reg }); + + const overflow_bit_ty = ty.structFieldType(1); + const overflow_bit_offset = @intCast(u32, ty.structFieldOffset(1, self.target.*)); + const cond_reg = try self.register_manager.allocReg(null, gp); + + // TODO handle floating point CCRs + assert(rwo.flag.ccr == .xcc or rwo.flag.ccr == .icc); + + _ = try self.addInst(.{ + .tag = .mov, + .data = .{ + .arithmetic_2op = .{ + .is_imm = false, + .rs1 = cond_reg, + .rs2_or_imm = .{ .rs2 = .g0 }, + }, + }, + }); + + _ = try self.addInst(.{ + .tag = .movcc, + .data = .{ + .conditional_move = .{ + .ccr = rwo.flag.ccr, + .cond = .{ .icond = rwo.flag.cond }, + .is_imm = true, + .rd = cond_reg, + .rs2_or_imm = .{ .imm = 1 }, + }, + }, + }); + try self.genSetStack(overflow_bit_ty, stack_offset - overflow_bit_offset, .{ + .register = cond_reg, + }); + }, .memory, .stack_offset => { switch (mcv) { .stack_offset => |off| { @@ -2647,7 +3103,7 @@ fn isErr(self: *Self, ty: Type, operand: MCValue) !MCValue { } }, }); - return MCValue{ .compare_flags_unsigned = .gt }; + return MCValue{ .compare_flags_unsigned = .{ .cmp = .gt, .ccr = .xcc } }; } else { return self.fail("TODO isErr for errors with size > 8", .{}); } @@ -2661,8 +3117,8 @@ fn isNonErr(self: *Self, ty: Type, operand: MCValue) !MCValue { const is_err_result = try self.isErr(ty, operand); switch (is_err_result) { .compare_flags_unsigned => |op| { - assert(op == .gt); - return MCValue{ .compare_flags_unsigned = .lte }; + assert(op.cmp == .gt); + return MCValue{ .compare_flags_unsigned = .{ .cmp = .lte, .ccr = op.ccr } }; }, .immediate => |imm| { assert(imm == 0); @@ -2714,6 +3170,7 @@ fn load(self: *Self, dst_mcv: MCValue, ptr: MCValue, ptr_ty: Type) InnerError!vo .dead => unreachable, .compare_flags_unsigned, .compare_flags_signed, + .register_with_overflow, => unreachable, // cannot hold an address .immediate => |imm| try self.setRegOrMem(elem_ty, dst_mcv, .{ .memory = imm }), .ptr_stack_offset => |off| try self.setRegOrMem(elem_ty, dst_mcv, .{ .stack_offset = off }), @@ -2801,6 +3258,7 @@ fn performReloc(self: *Self, inst: Mir.Inst.Index) !void { const tag = self.mir_instructions.items(.tag)[inst]; switch (tag) { .bpcc => self.mir_instructions.items(.data)[inst].branch_predict_int.inst = @intCast(Mir.Inst.Index, self.mir_instructions.len), + .bpr => self.mir_instructions.items(.data)[inst].branch_predict_reg.inst = @intCast(Mir.Inst.Index, self.mir_instructions.len), else => unreachable, } } @@ -2817,6 +3275,10 @@ fn processDeath(self: *Self, inst: Air.Inst.Index) void { .register => |reg| { self.register_manager.freeReg(reg); }, + .register_with_overflow => |rwo| { + self.register_manager.freeReg(rwo.reg); + self.compare_flags_inst = null; + }, .compare_flags_signed, .compare_flags_unsigned => { self.compare_flags_inst = null; }, @@ -2918,7 +3380,7 @@ fn resolveInst(self: *Self, inst: Air.Inst.Ref) InnerError!MCValue { const ref_int = @enumToInt(inst); if (ref_int < Air.Inst.Ref.typed_value_map.len) { const tv = Air.Inst.Ref.typed_value_map[ref_int]; - if (!tv.ty.hasRuntimeBits()) { + if (!tv.ty.hasRuntimeBits() and !tv.ty.isError()) { return MCValue{ .none = {} }; } return self.genTypedValue(tv); @@ -2926,7 +3388,7 @@ fn resolveInst(self: *Self, inst: Air.Inst.Ref) InnerError!MCValue { // If the type has no codegen bits, no need to store it. const inst_ty = self.air.typeOf(inst); - if (!inst_ty.hasRuntimeBits()) + if (!inst_ty.hasRuntimeBits() and !inst_ty.isError()) return MCValue{ .none = {} }; const inst_index = @intCast(Air.Inst.Index, ref_int - Air.Inst.Ref.typed_value_map.len); @@ -3017,14 +3479,14 @@ fn setRegOrMem(self: *Self, ty: Type, loc: MCValue, val: MCValue) !void { fn spillCompareFlagsIfOccupied(self: *Self) !void { if (self.compare_flags_inst) |inst_to_save| { const mcv = self.getResolvedInstValue(inst_to_save); - switch (mcv) { + const new_mcv = switch (mcv) { .compare_flags_signed, .compare_flags_unsigned, - => {}, + => try self.allocRegOrMem(inst_to_save, true), + .register_with_overflow => try self.allocRegOrMem(inst_to_save, false), else => unreachable, // mcv doesn't occupy the compare flags - } + }; - const new_mcv = try self.allocRegOrMem(inst_to_save, true); try self.setRegOrMem(self.air.typeOfIndex(inst_to_save), new_mcv, mcv); log.debug("spilling {d} to mcv {any}", .{ inst_to_save, new_mcv }); @@ -3032,6 +3494,13 @@ fn spillCompareFlagsIfOccupied(self: *Self) !void { try branch.inst_table.put(self.gpa, inst_to_save, new_mcv); self.compare_flags_inst = null; + + // TODO consolidate with register manager and spillInstruction + // this call should really belong in the register manager! + switch (mcv) { + .register_with_overflow => |rwo| self.register_manager.freeReg(rwo.reg), + else => {}, + } } } @@ -3055,6 +3524,7 @@ fn store(self: *Self, ptr: MCValue, value: MCValue, ptr_ty: Type, value_ty: Type .dead => unreachable, .compare_flags_unsigned, .compare_flags_signed, + .register_with_overflow, => unreachable, // cannot hold an address .immediate => |imm| { try self.setRegOrMem(value_ty, .{ .memory = imm }, value); @@ -3175,11 +3645,13 @@ fn truncRegister( }); }, 64 => { + if (dest_reg == operand_reg) + return; // Copy register to itself; nothing to do. _ = try self.addInst(.{ .tag = .mov, .data = .{ .arithmetic_2op = .{ - .is_imm = true, + .is_imm = false, .rs1 = dest_reg, .rs2_or_imm = .{ .rs2 = operand_reg }, }, diff --git a/src/arch/sparc64/Emit.zig b/src/arch/sparc64/Emit.zig index 1b40cc6e215bafd090ab2ebf1b1b7a54a1905e8a..5a082f163aed339f4b6b374cd75c70871b5cd353 100644 --- a/src/arch/sparc64/Emit.zig +++ b/src/arch/sparc64/Emit.zig @@ -79,6 +79,7 @@ pub fn emitMir( .dbg_epilogue_begin => try emit.mirDebugEpilogueBegin(), .add => try emit.mirArithmetic3Op(inst), + .addcc => try emit.mirArithmetic3Op(inst), .bpr => try emit.mirConditionalBranch(inst), .bpcc => try emit.mirConditionalBranch(inst), @@ -93,6 +94,10 @@ pub fn emitMir( .ldx => try emit.mirArithmetic3Op(inst), .@"or" => try emit.mirArithmetic3Op(inst), + .xor => try emit.mirArithmetic3Op(inst), + .xnor => try emit.mirArithmetic3Op(inst), + + .movcc => try emit.mirConditionalMove(inst), .mulx => try emit.mirArithmetic3Op(inst), @@ -125,6 +130,8 @@ pub fn emitMir( .cmp => try emit.mirArithmetic2Op(inst), .mov => try emit.mirArithmetic2Op(inst), + + .not => try emit.mirArithmetic2Op(inst), } } } @@ -185,6 +192,7 @@ fn mirArithmetic2Op(emit: *Emit, inst: Mir.Inst.Index) !void { .@"return" => try emit.writeInstruction(Instruction.@"return"(i13, rs1, imm)), .cmp => try emit.writeInstruction(Instruction.subcc(i13, rs1, imm, .g0)), .mov => try emit.writeInstruction(Instruction.@"or"(i13, .g0, imm, rs1)), + .not => try emit.writeInstruction(Instruction.xnor(i13, .g0, imm, rs1)), else => unreachable, } } else { @@ -193,6 +201,7 @@ fn mirArithmetic2Op(emit: *Emit, inst: Mir.Inst.Index) !void { .@"return" => try emit.writeInstruction(Instruction.@"return"(Register, rs1, rs2)), .cmp => try emit.writeInstruction(Instruction.subcc(Register, rs1, rs2, .g0)), .mov => try emit.writeInstruction(Instruction.@"or"(Register, .g0, rs2, rs1)), + .not => try emit.writeInstruction(Instruction.xnor(Register, .g0, rs2, rs1)), else => unreachable, } } @@ -209,12 +218,15 @@ fn mirArithmetic3Op(emit: *Emit, inst: Mir.Inst.Index) !void { const imm = data.rs2_or_imm.imm; switch (tag) { .add => try emit.writeInstruction(Instruction.add(i13, rs1, imm, rd)), + .addcc => try emit.writeInstruction(Instruction.addcc(i13, rs1, imm, rd)), .jmpl => try emit.writeInstruction(Instruction.jmpl(i13, rs1, imm, rd)), .ldub => try emit.writeInstruction(Instruction.ldub(i13, rs1, imm, rd)), .lduh => try emit.writeInstruction(Instruction.lduh(i13, rs1, imm, rd)), .lduw => try emit.writeInstruction(Instruction.lduw(i13, rs1, imm, rd)), .ldx => try emit.writeInstruction(Instruction.ldx(i13, rs1, imm, rd)), .@"or" => try emit.writeInstruction(Instruction.@"or"(i13, rs1, imm, rd)), + .xor => try emit.writeInstruction(Instruction.xor(i13, rs1, imm, rd)), + .xnor => try emit.writeInstruction(Instruction.xnor(i13, rs1, imm, rd)), .mulx => try emit.writeInstruction(Instruction.mulx(i13, rs1, imm, rd)), .save => try emit.writeInstruction(Instruction.save(i13, rs1, imm, rd)), .restore => try emit.writeInstruction(Instruction.restore(i13, rs1, imm, rd)), @@ -230,12 +242,15 @@ fn mirArithmetic3Op(emit: *Emit, inst: Mir.Inst.Index) !void { const rs2 = data.rs2_or_imm.rs2; switch (tag) { .add => try emit.writeInstruction(Instruction.add(Register, rs1, rs2, rd)), + .addcc => try emit.writeInstruction(Instruction.addcc(Register, rs1, rs2, rd)), .jmpl => try emit.writeInstruction(Instruction.jmpl(Register, rs1, rs2, rd)), .ldub => try emit.writeInstruction(Instruction.ldub(Register, rs1, rs2, rd)), .lduh => try emit.writeInstruction(Instruction.lduh(Register, rs1, rs2, rd)), .lduw => try emit.writeInstruction(Instruction.lduw(Register, rs1, rs2, rd)), .ldx => try emit.writeInstruction(Instruction.ldx(Register, rs1, rs2, rd)), .@"or" => try emit.writeInstruction(Instruction.@"or"(Register, rs1, rs2, rd)), + .xor => try emit.writeInstruction(Instruction.xor(Register, rs1, rs2, rd)), + .xnor => try emit.writeInstruction(Instruction.xnor(Register, rs1, rs2, rd)), .mulx => try emit.writeInstruction(Instruction.mulx(Register, rs1, rs2, rd)), .save => try emit.writeInstruction(Instruction.save(Register, rs1, rs2, rd)), .restore => try emit.writeInstruction(Instruction.restore(Register, rs1, rs2, rd)), @@ -294,6 +309,34 @@ fn mirConditionalBranch(emit: *Emit, inst: Mir.Inst.Index) !void { } } +fn mirConditionalMove(emit: *Emit, inst: Mir.Inst.Index) !void { + const tag = emit.mir.instructions.items(.tag)[inst]; + + switch (tag) { + .movcc => { + const data = emit.mir.instructions.items(.data)[inst].conditional_move; + if (data.is_imm) { + try emit.writeInstruction(Instruction.movcc( + i11, + data.cond, + data.ccr, + data.rs2_or_imm.imm, + data.rd, + )); + } else { + try emit.writeInstruction(Instruction.movcc( + Register, + data.cond, + data.ccr, + data.rs2_or_imm.rs2, + data.rd, + )); + } + }, + else => unreachable, + } +} + fn mirNop(emit: *Emit) !void { try emit.writeInstruction(Instruction.nop()); } diff --git a/src/arch/sparc64/Mir.zig b/src/arch/sparc64/Mir.zig index 2ef66a1fa4dae2f60ee49c9f81ca9925e12caa12..14867dde3060c4c1b77873d468a45ff06455b5c2 100644 --- a/src/arch/sparc64/Mir.zig +++ b/src/arch/sparc64/Mir.zig @@ -42,6 +42,7 @@ pub const Inst = struct { /// This uses the arithmetic_3op field. // TODO add other operations. add, + addcc, /// A.3 Branch on Integer Register with Prediction (BPr) /// This uses the branch_predict_reg field. @@ -73,6 +74,12 @@ pub const Inst = struct { /// This uses the arithmetic_3op field. // TODO add other operations. @"or", + xor, + xnor, + + /// A.35 Move Integer Register on Condition (MOVcc) + /// This uses the conditional_move field. + movcc, /// A.37 Multiply and Divide (64-bit) /// This uses the arithmetic_3op field. @@ -142,6 +149,13 @@ pub const Inst = struct { /// being the *destination* register. // TODO is it okay to abuse rs1 in this way? mov, // mov rs2/imm, rs1 -> or %g0, rs2/imm, rs1 + + /// Bitwise negation + /// This uses the arithmetic_2op field, with rs1 + /// being the *destination* register. + // TODO is it okay to abuse rs1 in this way? + // TODO this differs from official encoding for convenience, fix it later + not, // not rs2/imm, rs1 -> xnor %g0, rs2/imm, rs1 }; /// The position of an MIR instruction within the `Mir` instructions array. @@ -216,6 +230,22 @@ pub const Inst = struct { inst: Index, }, + /// Conditional move. + /// if is_imm true then it uses the imm field of rs2_or_imm, + /// otherwise it uses rs2 field. + /// + /// Used by e.g. movcc + conditional_move: struct { + is_imm: bool, + ccr: Instruction.CCR, + cond: Instruction.Condition, + rd: Register, + rs2_or_imm: union { + rs2: Register, + imm: i11, + }, + }, + /// No additional data /// /// Used by e.g. flushw diff --git a/src/arch/sparc64/abi.zig b/src/arch/sparc64/abi.zig index 6cdd183b36a0a7a8b31e00024690247d5d1e531a..a26ab9a20b7db94620f33797b195cb9817d4467f 100644 --- a/src/arch/sparc64/abi.zig +++ b/src/arch/sparc64/abi.zig @@ -13,6 +13,11 @@ pub const stack_bias = 2047; // The first 128 bytes of the stack is reserved for register saving purposes. // The ABI also requires to reserve space in the stack for the first six // outgoing arguments, even though they are usually passed in registers. +// TODO Don't allocate the argument space in leaf functions +// TODO Save an RO copy of outgoing arguments in reserved area when building in Debug +// TODO Should we also save it in ReleaseSafe? Solaris and OpenBSD binaries seem to ship +// with argument copying enabled and it doesn't seem to give them big slowdowns so +// I guess it would be okay to do in ReleaseSafe? pub const stack_reserved_area = 128 + 48; // There are no callee-preserved registers since the windowing diff --git a/src/arch/sparc64/bits.zig b/src/arch/sparc64/bits.zig index f4226b49da9be472da0024bf0a0dbde04fc10e67..7b0342e2a32392011eeb252fbbbc2918ce440195 100644 --- a/src/arch/sparc64/bits.zig +++ b/src/arch/sparc64/bits.zig @@ -644,7 +644,7 @@ pub const Instruction = union(enum) { .gt => .gu, .neq => .ne, .lt => .cs, - .lte => .le, + .lte => .leu, .eq => .eq, }; } @@ -1141,6 +1141,14 @@ pub const Instruction = union(enum) { }; } + pub fn addcc(comptime s2: type, rs1: Register, rs2: s2, rd: Register) Instruction { + return switch (s2) { + Register => format3a(0b10, 0b01_0000, rs1, rs2, rd), + i13 => format3b(0b10, 0b01_0000, rs1, rs2, rd), + else => unreachable, + }; + } + pub fn bpcc(cond: ICondition, annul: bool, pt: bool, ccr: CCR, disp: i21) Instruction { return format2c(0b001, .{ .icond = cond }, annul, pt, ccr, disp); } @@ -1197,6 +1205,30 @@ pub const Instruction = union(enum) { }; } + pub fn xor(comptime s2: type, rs1: Register, rs2: s2, rd: Register) Instruction { + return switch (s2) { + Register => format3a(0b10, 0b00_0011, rs1, rs2, rd), + i13 => format3b(0b10, 0b00_0011, rs1, rs2, rd), + else => unreachable, + }; + } + + pub fn xnor(comptime s2: type, rs1: Register, rs2: s2, rd: Register) Instruction { + return switch (s2) { + Register => format3a(0b10, 0b00_0111, rs1, rs2, rd), + i13 => format3b(0b10, 0b00_0111, rs1, rs2, rd), + else => unreachable, + }; + } + + pub fn movcc(comptime s2: type, cond: Condition, ccr: CCR, rs2: s2, rd: Register) Instruction { + return switch (s2) { + Register => format4c(0b10_1100, cond, ccr, rs2, rd), + i11 => format4d(0b10_1100, cond, ccr, rs2, rd), + else => unreachable, + }; + } + pub fn mulx(comptime s2: type, rs1: Register, rs2: s2, rd: Register) Instruction { return switch (s2) { Register => format3a(0b10, 0b00_1001, rs1, rs2, rd), diff --git a/test/behavior/align.zig b/test/behavior/align.zig index ad35db21718f64bce8e235bcf0ff74ead1632559..056354f23772fc271548ee47c0d276b836abc274 100644 --- a/test/behavior/align.zig +++ b/test/behavior/align.zig @@ -505,6 +505,7 @@ test "align(N) on functions" { if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO // function alignment is a compile error on wasm32/wasm64 if (native_arch == .wasm32 or native_arch == .wasm64) return error.SkipZigTest;