Legalize: replace safety_checked_instructions

This adds 4 `Legalize.Feature`s:
* `expand_intcast_safe`
* `expand_add_safe`
* `expand_sub_safe`
* `expand_mul_safe`

These do pretty much what they say on the tin. This logic was previously
in Sema, used when `Zcu.Feature.safety_checked_instructions` was not
supported by the backend. That `Zcu.Feature` has been removed in favour
of this legalization.
This commit is contained in:
mlugg 2025-05-30 17:12:11 +01:00
parent 77e6513030
commit 4c4dacf81a
No known key found for this signature in database
GPG Key ID: 3F5B7DCCBF4AF02E
10 changed files with 559 additions and 245 deletions

View File

@ -50,8 +50,6 @@ pub const Inst = struct {
/// is the same as both operands.
/// The panic handler function must be populated before lowering AIR
/// that contains this instruction.
/// This instruction will only be emitted if the backend has the
/// feature `safety_checked_instructions`.
/// Uses the `bin_op` field.
add_safe,
/// Float addition. The instruction is allowed to have equal or more
@ -79,8 +77,6 @@ pub const Inst = struct {
/// is the same as both operands.
/// The panic handler function must be populated before lowering AIR
/// that contains this instruction.
/// This instruction will only be emitted if the backend has the
/// feature `safety_checked_instructions`.
/// Uses the `bin_op` field.
sub_safe,
/// Float subtraction. The instruction is allowed to have equal or more
@ -108,8 +104,6 @@ pub const Inst = struct {
/// is the same as both operands.
/// The panic handler function must be populated before lowering AIR
/// that contains this instruction.
/// This instruction will only be emitted if the backend has the
/// feature `safety_checked_instructions`.
/// Uses the `bin_op` field.
mul_safe,
/// Float multiplication. The instruction is allowed to have equal or more

View File

@ -81,6 +81,19 @@ pub const Feature = enum {
/// Legalize reduce of a one element vector to a bitcast
reduce_one_elem_to_bitcast,
/// Replace `intcast_safe` with an explicit safety check which `call`s the panic function on failure.
/// Not compatible with `scalarize_intcast_safe`.
expand_intcast_safe,
/// Replace `add_safe` with an explicit safety check which `call`s the panic function on failure.
/// Not compatible with `scalarize_add_safe`.
expand_add_safe,
/// Replace `sub_safe` with an explicit safety check which `call`s the panic function on failure.
/// Not compatible with `scalarize_sub_safe`.
expand_sub_safe,
/// Replace `mul_safe` with an explicit safety check which `call`s the panic function on failure.
/// Not compatible with `scalarize_mul_safe`.
expand_mul_safe,
fn scalarize(tag: Air.Inst.Tag) Feature {
return switch (tag) {
else => unreachable,
@ -205,17 +218,14 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
.arg,
=> {},
inline .add,
.add_safe,
.add_optimized,
.add_wrap,
.add_sat,
.sub,
.sub_safe,
.sub_optimized,
.sub_wrap,
.sub_sat,
.mul,
.mul_safe,
.mul_optimized,
.mul_wrap,
.mul_sat,
@ -240,6 +250,27 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op;
if (l.typeOf(bin_op.lhs).isVector(zcu)) continue :inst try l.scalarize(inst, .bin_op);
},
.add_safe => if (l.features.contains(.expand_add_safe)) {
assert(!l.features.contains(.scalarize_add_safe)); // it doesn't make sense to do both
continue :inst l.replaceInst(inst, .block, try l.safeArithmeticBlockPayload(inst, .add_with_overflow));
} else if (l.features.contains(.scalarize_add_safe)) {
const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op;
if (l.typeOf(bin_op.lhs).isVector(zcu)) continue :inst try l.scalarize(inst, .bin_op);
},
.sub_safe => if (l.features.contains(.expand_sub_safe)) {
assert(!l.features.contains(.scalarize_sub_safe)); // it doesn't make sense to do both
continue :inst l.replaceInst(inst, .block, try l.safeArithmeticBlockPayload(inst, .sub_with_overflow));
} else if (l.features.contains(.scalarize_sub_safe)) {
const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op;
if (l.typeOf(bin_op.lhs).isVector(zcu)) continue :inst try l.scalarize(inst, .bin_op);
},
.mul_safe => if (l.features.contains(.expand_mul_safe)) {
assert(!l.features.contains(.scalarize_mul_safe)); // it doesn't make sense to do both
continue :inst l.replaceInst(inst, .block, try l.safeArithmeticBlockPayload(inst, .mul_with_overflow));
} else if (l.features.contains(.scalarize_mul_safe)) {
const bin_op = l.air_instructions.items(.data)[@intFromEnum(inst)].bin_op;
if (l.typeOf(bin_op.lhs).isVector(zcu)) continue :inst try l.scalarize(inst, .bin_op);
},
.ptr_add,
.ptr_sub,
.add_with_overflow,
@ -295,7 +326,6 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
.fptrunc,
.fpext,
.intcast,
.intcast_safe,
.trunc,
.int_from_float,
.int_from_float_optimized,
@ -312,6 +342,13 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
if (to_ty.isVector(zcu) and from_ty.isVector(zcu) and to_ty.vectorLen(zcu) == from_ty.vectorLen(zcu))
continue :inst try l.scalarize(inst, .ty_op);
},
.intcast_safe => if (l.features.contains(.expand_intcast_safe)) {
assert(!l.features.contains(.scalarize_intcast_safe)); // it doesn't make sense to do both
continue :inst l.replaceInst(inst, .block, try l.safeIntcastBlockPayload(inst));
} else if (l.features.contains(.scalarize_intcast_safe)) {
const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
if (ty_op.ty.toType().isVector(zcu)) continue :inst try l.scalarize(inst, .ty_op);
},
.block,
.loop,
=> {
@ -550,81 +587,83 @@ fn scalarizeBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, comptime data_
const expected_instructions_len = l.air_instructions.len + (6 + arity + 8);
try l.air_instructions.ensureTotalCapacity(gpa, expected_instructions_len);
var res_block: Block(4) = .empty;
var res_block_buf: [4]Air.Inst.Index = undefined;
var res_block: Block = .init(&res_block_buf);
{
const res_alloc_inst = res_block.add(l.addInstAssumeCapacity(.{
const res_alloc_inst = res_block.add(l, .{
.tag = .alloc,
.data = .{ .ty = try pt.singleMutPtrType(res_ty) },
}));
const index_alloc_inst = res_block.add(l.addInstAssumeCapacity(.{
});
const index_alloc_inst = res_block.add(l, .{
.tag = .alloc,
.data = .{ .ty = .ptr_usize },
}));
_ = res_block.add(l.addInstAssumeCapacity(.{
});
_ = res_block.add(l, .{
.tag = .store,
.data = .{ .bin_op = .{
.lhs = index_alloc_inst.toRef(),
.rhs = .zero_usize,
} },
}));
});
const loop_inst: Air.Inst.Index = @enumFromInt(l.air_instructions.len + (3 + arity + 7));
var loop_block: Block(3 + arity + 2) = .empty;
var loop_block_buf: [3 + arity + 2]Air.Inst.Index = undefined;
var loop_block: Block = .init(&loop_block_buf);
{
const cur_index_inst = loop_block.add(l.addInstAssumeCapacity(.{
const cur_index_inst = loop_block.add(l, .{
.tag = .load,
.data = .{ .ty_op = .{
.ty = .usize_type,
.operand = index_alloc_inst.toRef(),
} },
}));
_ = loop_block.add(l.addInstAssumeCapacity(.{
});
_ = loop_block.add(l, .{
.tag = .vector_store_elem,
.data = .{ .vector_store_elem = .{
.vector_ptr = res_alloc_inst.toRef(),
.payload = try l.addExtra(Air.Bin, .{
.lhs = cur_index_inst.toRef(),
.rhs = loop_block.add(l.addInstAssumeCapacity(res_elem: switch (data_tag) {
.rhs = loop_block.add(l, res_elem: switch (data_tag) {
.un_op => .{
.tag = orig.tag,
.data = .{ .un_op = loop_block.add(l.addInstAssumeCapacity(.{
.data = .{ .un_op = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = orig.data.un_op,
.rhs = cur_index_inst.toRef(),
} },
})).toRef() },
}).toRef() },
},
.ty_op => .{
.tag = orig.tag,
.data = .{ .ty_op = .{
.ty = Air.internedToRef(orig.data.ty_op.ty.toType().scalarType(zcu).toIntern()),
.operand = loop_block.add(l.addInstAssumeCapacity(.{
.operand = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = orig.data.ty_op.operand,
.rhs = cur_index_inst.toRef(),
} },
})).toRef(),
}).toRef(),
} },
},
.bin_op => .{
.tag = orig.tag,
.data = .{ .bin_op = .{
.lhs = loop_block.add(l.addInstAssumeCapacity(.{
.lhs = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = orig.data.bin_op.lhs,
.rhs = cur_index_inst.toRef(),
} },
})).toRef(),
.rhs = loop_block.add(l.addInstAssumeCapacity(.{
}).toRef(),
.rhs = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = orig.data.bin_op.rhs,
.rhs = cur_index_inst.toRef(),
} },
})).toRef(),
}).toRef(),
} },
},
.ty_pl_vector_cmp => {
@ -650,20 +689,20 @@ fn scalarizeBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, comptime data_
},
},
.data = .{ .bin_op = .{
.lhs = loop_block.add(l.addInstAssumeCapacity(.{
.lhs = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = extra.lhs,
.rhs = cur_index_inst.toRef(),
} },
})).toRef(),
.rhs = loop_block.add(l.addInstAssumeCapacity(.{
}).toRef(),
.rhs = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = extra.rhs,
.rhs = cur_index_inst.toRef(),
} },
})).toRef(),
}).toRef(),
} },
};
},
@ -673,94 +712,96 @@ fn scalarizeBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, comptime data_
.tag = orig.tag,
.data = .{ .pl_op = .{
.payload = try l.addExtra(Air.Bin, .{
.lhs = loop_block.add(l.addInstAssumeCapacity(.{
.lhs = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = extra.lhs,
.rhs = cur_index_inst.toRef(),
} },
})).toRef(),
.rhs = loop_block.add(l.addInstAssumeCapacity(.{
}).toRef(),
.rhs = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = extra.rhs,
.rhs = cur_index_inst.toRef(),
} },
})).toRef(),
}).toRef(),
}),
.operand = loop_block.add(l.addInstAssumeCapacity(.{
.operand = loop_block.add(l, .{
.tag = .array_elem_val,
.data = .{ .bin_op = .{
.lhs = orig.data.pl_op.operand,
.rhs = cur_index_inst.toRef(),
} },
})).toRef(),
}).toRef(),
} },
};
},
})).toRef(),
}).toRef(),
}),
} },
}));
const not_done_inst = loop_block.add(l.addInstAssumeCapacity(.{
});
const not_done_inst = loop_block.add(l, .{
.tag = .cmp_lt,
.data = .{ .bin_op = .{
.lhs = cur_index_inst.toRef(),
.rhs = try pt.intRef(.usize, res_ty.vectorLen(zcu) - 1),
} },
}));
});
var not_done_block: Block(3) = .empty;
var not_done_block_buf: [3]Air.Inst.Index = undefined;
var not_done_block: Block = .init(&not_done_block_buf);
{
_ = not_done_block.add(l.addInstAssumeCapacity(.{
_ = not_done_block.add(l, .{
.tag = .store,
.data = .{ .bin_op = .{
.lhs = index_alloc_inst.toRef(),
.rhs = not_done_block.add(l.addInstAssumeCapacity(.{
.rhs = not_done_block.add(l, .{
.tag = .add,
.data = .{ .bin_op = .{
.lhs = cur_index_inst.toRef(),
.rhs = .one_usize,
} },
})).toRef(),
}).toRef(),
} },
}));
_ = not_done_block.add(l.addInstAssumeCapacity(.{
});
_ = not_done_block.add(l, .{
.tag = .repeat,
.data = .{ .repeat = .{ .loop_inst = loop_inst } },
}));
});
}
var done_block: Block(2) = .empty;
var done_block_buf: [2]Air.Inst.Index = undefined;
var done_block: Block = .init(&done_block_buf);
{
_ = done_block.add(l.addInstAssumeCapacity(.{
_ = done_block.add(l, .{
.tag = .br,
.data = .{ .br = .{
.block_inst = orig_inst,
.operand = done_block.add(l.addInstAssumeCapacity(.{
.operand = done_block.add(l, .{
.tag = .load,
.data = .{ .ty_op = .{
.ty = Air.internedToRef(res_ty.toIntern()),
.operand = res_alloc_inst.toRef(),
} },
})).toRef(),
}).toRef(),
} },
}));
});
}
_ = loop_block.add(l.addInstAssumeCapacity(.{
_ = loop_block.add(l, .{
.tag = .cond_br,
.data = .{ .pl_op = .{
.operand = not_done_inst.toRef(),
.payload = try l.addCondBrBodies(not_done_block.body(), done_block.body()),
} },
}));
});
}
assert(loop_inst == res_block.add(l.addInstAssumeCapacity(.{
assert(loop_inst == res_block.add(l, .{
.tag = .loop,
.data = .{ .ty_pl = .{
.ty = .noreturn_type,
.payload = try l.addBlockBody(loop_block.body()),
} },
})));
}));
}
assert(l.air_instructions.len == expected_instructions_len);
return .{ .ty_pl = .{
@ -768,29 +809,423 @@ fn scalarizeBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, comptime data_
.payload = try l.addBlockBody(res_block.body()),
} };
}
fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.Inst.Data {
const pt = l.pt;
const zcu = pt.zcu;
const gpa = zcu.gpa;
const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;
fn Block(comptime capacity: usize) type {
return struct {
instructions: [capacity]Air.Inst.Index,
len: usize,
const operand_ref = ty_op.operand;
const operand_ty = l.typeOf(operand_ref);
const dest_ty = ty_op.ty.toType();
const empty: @This() = .{
.instructions = undefined,
const is_vector = operand_ty.zigTypeTag(zcu) == .vector;
const operand_scalar_ty = operand_ty.scalarType(zcu);
const dest_scalar_ty = dest_ty.scalarType(zcu);
assert(operand_scalar_ty.zigTypeTag(zcu) == .int);
const dest_is_enum = switch (dest_scalar_ty.zigTypeTag(zcu)) {
.int => false,
.@"enum" => true,
else => unreachable,
};
const operand_info = operand_scalar_ty.intInfo(zcu);
const dest_info = dest_scalar_ty.intInfo(zcu);
const have_min_check, const have_max_check = c: {
const dest_pos_bits = dest_info.bits - @intFromBool(dest_info.signedness == .signed);
const operand_pos_bits = operand_info.bits - @intFromBool(operand_info.signedness == .signed);
const dest_allows_neg = dest_info.signedness == .signed and dest_info.bits > 0;
const operand_allows_neg = operand_info.signedness == .signed and operand_info.bits > 0;
break :c .{
operand_allows_neg and (!dest_allows_neg or dest_info.bits < operand_info.bits),
dest_pos_bits < operand_pos_bits,
};
};
// The worst-case scenario in terms of total instructions and total condbrs is the case where
// the result type is an exhaustive enum whose tag type is smaller than the operand type:
//
// %x = block({
// %1 = cmp_lt(%y, @min_allowed_int)
// %2 = cmp_gt(%y, @max_allowed_int)
// %3 = bool_or(%1, %2)
// %4 = cond_br(%3, {
// %5 = call(@panic.invalidEnumValue, [])
// %6 = unreach()
// }, {
// %7 = intcast(@res_ty, %y)
// %8 = is_named_enum_value(%7)
// %9 = cond_br(%8, {
// %10 = br(%x, %7)
// }, {
// %11 = call(@panic.invalidEnumValue, [])
// %12 = unreach()
// })
// })
// })
//
// Note that vectors of enums don't exist -- the worst case for vectors is this:
//
// %x = block({
// %1 = cmp_lt(%y, @min_allowed_int)
// %2 = cmp_gt(%y, @max_allowed_int)
// %3 = bool_or(%1, %2)
// %4 = reduce(%3, .@"or")
// %5 = cond_br(%4, {
// %6 = call(@panic.invalidEnumValue, [])
// %7 = unreach()
// }, {
// %8 = intcast(@res_ty, %y)
// %9 = br(%x, %8)
// })
// })
try l.air_instructions.ensureUnusedCapacity(gpa, 12);
var body_inst_buf: [12]Air.Inst.Index = undefined;
var condbr_buf: [2]CondBr = undefined;
var condbr_idx: usize = 0;
var main_block: Block = .init(&body_inst_buf);
var cur_block: *Block = &main_block;
const panic_id: Zcu.SimplePanicId = if (dest_is_enum) .invalid_enum_value else .cast_truncated_data;
if (have_min_check or have_max_check) {
const dest_int_ty = if (dest_is_enum) dest_ty.intTagType(zcu) else dest_ty;
const condbr = &condbr_buf[condbr_idx];
condbr_idx += 1;
const below_min_inst: Air.Inst.Index = if (have_min_check) inst: {
const min_val_ref = Air.internedToRef((try dest_int_ty.minInt(pt, operand_ty)).toIntern());
break :inst try cur_block.addCmp(l, is_vector, .lt, operand_ref, min_val_ref);
} else undefined;
const above_max_inst: Air.Inst.Index = if (have_max_check) inst: {
const max_val_ref = Air.internedToRef((try dest_int_ty.maxInt(pt, operand_ty)).toIntern());
break :inst try cur_block.addCmp(l, is_vector, .gt, operand_ref, max_val_ref);
} else undefined;
const out_of_range_inst: Air.Inst.Index = inst: {
if (have_min_check and have_max_check) break :inst cur_block.add(l, .{
.tag = .bool_or,
.data = .{ .bin_op = .{
.lhs = below_min_inst.toRef(),
.rhs = above_max_inst.toRef(),
} },
});
if (have_min_check) break :inst below_min_inst;
if (have_max_check) break :inst above_max_inst;
unreachable;
};
const scalar_out_of_range_inst: Air.Inst.Index = if (is_vector) cur_block.add(l, .{
.tag = .reduce,
.data = .{ .reduce = .{
.operand = out_of_range_inst.toRef(),
.operation = .Or,
} },
}) else out_of_range_inst;
condbr.* = .init(l, scalar_out_of_range_inst.toRef(), cur_block, .{
.true = .cold,
.false = .none,
});
condbr.then_block = .init(cur_block.stealRemainingCapacity());
try condbr.then_block.addPanic(l, panic_id);
condbr.else_block = .init(condbr.then_block.stealRemainingCapacity());
cur_block = &condbr.else_block;
}
// Now we know we're in-range, we can intcast:
const cast_inst = cur_block.add(l, .{
.tag = .intcast,
.data = .{ .ty_op = .{
.ty = Air.internedToRef(dest_ty.toIntern()),
.operand = operand_ref,
} },
});
// For ints we're already done, but for exhaustive enums we must check this is a valid tag.
if (dest_is_enum and !dest_ty.isNonexhaustiveEnum(zcu) and zcu.backendSupportsFeature(.is_named_enum_value)) {
assert(!is_vector); // vectors of enums don't exist
// We are building this:
// %1 = is_named_enum_value(%cast_inst)
// %2 = cond_br(%1, {
// <new cursor>
// }, {
// <panic>
// })
const is_named_inst = cur_block.add(l, .{
.tag = .is_named_enum_value,
.data = .{ .un_op = cast_inst.toRef() },
});
const condbr = &condbr_buf[condbr_idx];
condbr_idx += 1;
condbr.* = .init(l, is_named_inst.toRef(), cur_block, .{
.true = .none,
.false = .cold,
});
condbr.else_block = .init(cur_block.stealRemainingCapacity());
try condbr.else_block.addPanic(l, panic_id);
condbr.then_block = .init(condbr.else_block.stealRemainingCapacity());
cur_block = &condbr.then_block;
}
// Finally, just `br` to our outer `block`.
_ = cur_block.add(l, .{
.tag = .br,
.data = .{ .br = .{
.block_inst = orig_inst,
.operand = cast_inst.toRef(),
} },
});
// We might not have used all of the instructions; that's intentional.
_ = cur_block.stealRemainingCapacity();
for (condbr_buf[0..condbr_idx]) |*condbr| try condbr.finish(l);
return .{ .ty_pl = .{
.ty = Air.internedToRef(dest_ty.toIntern()),
.payload = try l.addBlockBody(main_block.body()),
} };
}
fn safeArithmeticBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, overflow_op_tag: Air.Inst.Tag) Error!Air.Inst.Data {
const pt = l.pt;
const zcu = pt.zcu;
const gpa = zcu.gpa;
const bin_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].bin_op;
const operand_ty = l.typeOf(bin_op.lhs);
assert(l.typeOf(bin_op.rhs).toIntern() == operand_ty.toIntern());
const is_vector = operand_ty.zigTypeTag(zcu) == .vector;
const overflow_tuple_ty = try pt.overflowArithmeticTupleType(operand_ty);
const overflow_bits_ty = overflow_tuple_ty.fieldType(1, zcu);
// The worst-case scenario is a vector operand:
//
// %1 = add_with_overflow(%x, %y)
// %2 = struct_field_val(%1, .@"1")
// %3 = reduce(%2, .@"or")
// %4 = bitcast(%3, @bool_type)
// %5 = cond_br(%4, {
// %6 = call(@panic.integerOverflow, [])
// %7 = unreach()
// }, {
// %8 = struct_field_val(%1, .@"0")
// %9 = br(%z, %8)
// })
try l.air_instructions.ensureUnusedCapacity(gpa, 9);
var body_inst_buf: [9]Air.Inst.Index = undefined;
var main_block: Block = .init(&body_inst_buf);
const overflow_op_inst = main_block.add(l, .{
.tag = overflow_op_tag,
.data = .{ .ty_pl = .{
.ty = Air.internedToRef(overflow_tuple_ty.toIntern()),
.payload = try l.addExtra(Air.Bin, .{
.lhs = bin_op.lhs,
.rhs = bin_op.rhs,
}),
} },
});
const overflow_bits_inst = main_block.add(l, .{
.tag = .struct_field_val,
.data = .{ .ty_pl = .{
.ty = Air.internedToRef(overflow_bits_ty.toIntern()),
.payload = try l.addExtra(Air.StructField, .{
.struct_operand = overflow_op_inst.toRef(),
.field_index = 1,
}),
} },
});
const any_overflow_bit_inst = if (is_vector) main_block.add(l, .{
.tag = .reduce,
.data = .{ .reduce = .{
.operand = overflow_bits_inst.toRef(),
.operation = .Or,
} },
}) else overflow_bits_inst;
const any_overflow_inst = try main_block.addCmp(l, false, .eq, any_overflow_bit_inst.toRef(), .one_u1);
var condbr: CondBr = .init(l, any_overflow_inst.toRef(), &main_block, .{
.true = .cold,
.false = .none,
});
condbr.then_block = .init(main_block.stealRemainingCapacity());
try condbr.then_block.addPanic(l, .integer_overflow);
condbr.else_block = .init(condbr.then_block.stealRemainingCapacity());
const result_inst = condbr.else_block.add(l, .{
.tag = .struct_field_val,
.data = .{ .ty_pl = .{
.ty = Air.internedToRef(operand_ty.toIntern()),
.payload = try l.addExtra(Air.StructField, .{
.struct_operand = overflow_op_inst.toRef(),
.field_index = 0,
}),
} },
});
_ = condbr.else_block.add(l, .{
.tag = .br,
.data = .{ .br = .{
.block_inst = orig_inst,
.operand = result_inst.toRef(),
} },
});
// We might not have used all of the instructions; that's intentional.
_ = condbr.else_block.stealRemainingCapacity();
try condbr.finish(l);
return .{ .ty_pl = .{
.ty = Air.internedToRef(operand_ty.toIntern()),
.payload = try l.addBlockBody(main_block.body()),
} };
}
const Block = struct {
instructions: []Air.Inst.Index,
len: usize,
/// There are two common usages of the API:
/// * `buf.len` is exactly the number of instructions which will be in this block
/// * `buf.len` is no smaller than necessary, and `b.stealRemainingCapacity` will be used
fn init(buf: []Air.Inst.Index) Block {
return .{
.instructions = buf,
.len = 0,
};
}
fn add(b: *@This(), inst: Air.Inst.Index) Air.Inst.Index {
b.instructions[b.len] = inst;
b.len += 1;
return inst;
}
/// Like `Legalize.addInstAssumeCapacity`, but also appends the instruction to `b`.
fn add(b: *Block, l: *Legalize, inst_data: Air.Inst) Air.Inst.Index {
const inst = l.addInstAssumeCapacity(inst_data);
b.instructions[b.len] = inst;
b.len += 1;
return inst;
}
fn body(b: *const @This()) []const Air.Inst.Index {
assert(b.len == b.instructions.len);
return &b.instructions;
/// Adds the code to call the panic handler `panic_id`. This is usually `.call` then `.unreach`,
/// but if `Zcu.Feature.panic_fn` is unsupported, we lower to `.trap` instead.
fn addPanic(b: *Block, l: *Legalize, panic_id: Zcu.SimplePanicId) Error!void {
const zcu = l.pt.zcu;
if (!zcu.backendSupportsFeature(.panic_fn)) {
_ = b.add(l, .{
.tag = .trap,
.data = .{ .no_op = {} },
});
return;
}
const panic_fn_val = zcu.builtin_decl_values.get(panic_id.toBuiltin());
_ = b.add(l, .{
.tag = .call,
.data = .{ .pl_op = .{
.operand = Air.internedToRef(panic_fn_val),
.payload = try l.addExtra(Air.Call, .{ .args_len = 0 }),
} },
});
_ = b.add(l, .{
.tag = .unreach,
.data = .{ .no_op = {} },
});
}
/// Adds a `cmp_*` instruction (including maybe `cmp_vector`) to `b`. This is a fairly thin wrapper
/// around `add`, although it does compute the result type if `is_vector` (`@Vector(n, bool)`).
fn addCmp(
b: *Block,
l: *Legalize,
is_vector: bool,
op: std.math.CompareOperator,
lhs: Air.Inst.Ref,
rhs: Air.Inst.Ref,
) Error!Air.Inst.Index {
const pt = l.pt;
if (is_vector) {
const bool_vec_ty = try pt.vectorType(.{
.child = .bool_type,
.len = l.typeOf(lhs).vectorLen(pt.zcu),
});
return b.add(l, .{
.tag = .cmp_vector,
.data = .{ .ty_pl = .{
.ty = Air.internedToRef(bool_vec_ty.toIntern()),
.payload = try l.addExtra(Air.VectorCmp, .{
.lhs = lhs,
.rhs = rhs,
.op = Air.VectorCmp.encodeOp(op),
}),
} },
});
}
return b.add(l, .{
.tag = switch (op) {
.lt => .cmp_lt,
.lte => .cmp_lte,
.eq => .cmp_eq,
.gte => .cmp_gte,
.gt => .cmp_gt,
.neq => .cmp_neq,
},
.data = .{ .bin_op = .{
.lhs = lhs,
.rhs = rhs,
} },
});
}
/// Returns the unused capacity of `b.instructions`, and shrinks `b.instructions` down to `b.len`.
/// This is useful when you've provided a buffer big enough for all your instructions, but you are
/// now starting a new block and some of them need to live there instead.
fn stealRemainingCapacity(b: *Block) []Air.Inst.Index {
const remaining = b.instructions[b.len..];
b.instructions = b.instructions[0..b.len];
return remaining;
}
fn body(b: *const Block) []const Air.Inst.Index {
assert(b.len == b.instructions.len);
return b.instructions;
}
};
const CondBr = struct {
inst: Air.Inst.Index,
hints: BranchHints,
then_block: Block,
else_block: Block,
const BranchHints = struct {
true: std.builtin.BranchHint,
false: std.builtin.BranchHint,
};
}
/// The return value has `then_block` and `else_block` initialized to `undefined`; it is the
/// caller's reponsibility to initialize them.
fn init(l: *Legalize, operand: Air.Inst.Ref, parent_block: *Block, hints: BranchHints) CondBr {
return .{
.inst = parent_block.add(l, .{
.tag = .cond_br,
.data = .{ .pl_op = .{
.operand = operand,
.payload = undefined,
} },
}),
.hints = hints,
.then_block = undefined,
.else_block = undefined,
};
}
fn finish(cond_br: CondBr, l: *Legalize) Error!void {
const data = &l.air_instructions.items(.data)[@intFromEnum(cond_br.inst)];
data.pl_op.payload = try l.addCondBrBodiesHints(
cond_br.then_block.body(),
cond_br.else_block.body(),
.{
.true = cond_br.hints.true,
.false = cond_br.hints.false,
.then_cov = .none,
.else_cov = .none,
},
);
}
};
fn addInstAssumeCapacity(l: *Legalize, inst: Air.Inst) Air.Inst.Index {
defer l.air_instructions.appendAssumeCapacity(inst);
@ -818,17 +1253,20 @@ fn addBlockBody(l: *Legalize, body: []const Air.Inst.Index) Error!u32 {
}
fn addCondBrBodies(l: *Legalize, then_body: []const Air.Inst.Index, else_body: []const Air.Inst.Index) Error!u32 {
return l.addCondBrBodiesHints(then_body, else_body, .{
.true = .none,
.false = .none,
.then_cov = .none,
.else_cov = .none,
});
}
fn addCondBrBodiesHints(l: *Legalize, then_body: []const Air.Inst.Index, else_body: []const Air.Inst.Index, hints: Air.CondBr.BranchHints) Error!u32 {
try l.air_extra.ensureUnusedCapacity(l.pt.zcu.gpa, 3 + then_body.len + else_body.len);
defer {
l.air_extra.appendSliceAssumeCapacity(&.{
@intCast(then_body.len),
@intCast(else_body.len),
@bitCast(Air.CondBr.BranchHints{
.true = .none,
.false = .none,
.then_cov = .none,
.else_cov = .none,
}),
@bitCast(hints),
});
l.air_extra.appendSliceAssumeCapacity(@ptrCast(then_body));
l.air_extra.appendSliceAssumeCapacity(@ptrCast(else_body));

View File

@ -8912,21 +8912,10 @@ fn zirEnumFromInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
try sema.requireRuntimeBlock(block, src, operand_src);
if (block.wantSafety()) {
if (zcu.backendSupportsFeature(.safety_checked_instructions)) {
if (zcu.backendSupportsFeature(.panic_fn)) {
_ = try sema.preparePanicId(src, .invalid_enum_value);
return block.addTyOp(.intcast_safe, dest_ty, operand);
} else {
// Slightly silly fallback case...
const int_tag_ty = dest_ty.intTagType(zcu);
// Use `intCast`, since it'll set up the Sema-emitted safety checks for us!
const int_val = try sema.intCast(block, src, int_tag_ty, src, operand, src, true, true);
const result = try block.addBitCast(dest_ty, int_val);
if (!dest_ty.isNonexhaustiveEnum(zcu) and zcu.backendSupportsFeature(.is_named_enum_value)) {
const ok = try block.addUnOp(.is_named_enum_value, result);
try sema.addSafetyCheck(block, src, ok, .invalid_enum_value);
}
return result;
}
return block.addTyOp(.intcast_safe, dest_ty, operand);
}
return block.addTyOp(.intcast, dest_ty, operand);
}
@ -10331,90 +10320,11 @@ fn intCast(
try sema.requireRuntimeBlock(block, src, operand_src);
if (runtime_safety and block.wantSafety()) {
if (zcu.backendSupportsFeature(.safety_checked_instructions)) {
if (zcu.backendSupportsFeature(.panic_fn)) {
_ = try sema.preparePanicId(src, .negative_to_unsigned);
_ = try sema.preparePanicId(src, .cast_truncated_data);
return block.addTyOp(.intcast_safe, dest_ty, operand);
}
const actual_info = operand_scalar_ty.intInfo(zcu);
const wanted_info = dest_scalar_ty.intInfo(zcu);
const actual_bits = actual_info.bits;
const wanted_bits = wanted_info.bits;
const actual_value_bits = actual_bits - @intFromBool(actual_info.signedness == .signed);
const wanted_value_bits = wanted_bits - @intFromBool(wanted_info.signedness == .signed);
// range shrinkage
// requirement: int value fits into target type
if (wanted_value_bits < actual_value_bits) {
const dest_max_val_scalar = try dest_scalar_ty.maxIntScalar(pt, operand_scalar_ty);
const dest_max_val = try sema.splat(operand_ty, dest_max_val_scalar);
const dest_max = Air.internedToRef(dest_max_val.toIntern());
if (actual_info.signedness == .signed) {
const diff = try block.addBinOp(.sub_wrap, dest_max, operand);
// Reinterpret the sign-bit as part of the value. This will make
// negative differences (`operand` > `dest_max`) appear too big.
const unsigned_scalar_operand_ty = try pt.intType(.unsigned, actual_bits);
const unsigned_operand_ty = if (is_vector) try pt.vectorType(.{
.len = dest_ty.vectorLen(zcu),
.child = unsigned_scalar_operand_ty.toIntern(),
}) else unsigned_scalar_operand_ty;
const diff_unsigned = try block.addBitCast(unsigned_operand_ty, diff);
// If the destination type is signed, then we need to double its
// range to account for negative values.
const dest_range_val = if (wanted_info.signedness == .signed) range_val: {
const one_scalar = try pt.intValue(unsigned_scalar_operand_ty, 1);
const one = if (is_vector) Value.fromInterned(try pt.intern(.{ .aggregate = .{
.ty = unsigned_operand_ty.toIntern(),
.storage = .{ .repeated_elem = one_scalar.toIntern() },
} })) else one_scalar;
const range_minus_one = try dest_max_val.shl(one, unsigned_operand_ty, sema.arena, pt);
const result = try arith.addWithOverflow(sema, unsigned_operand_ty, range_minus_one, one);
assert(result.overflow_bit.compareAllWithZero(.eq, zcu));
break :range_val result.wrapped_result;
} else try pt.getCoerced(dest_max_val, unsigned_operand_ty);
const dest_range = Air.internedToRef(dest_range_val.toIntern());
const ok = if (is_vector) ok: {
const is_in_range = try block.addCmpVector(diff_unsigned, dest_range, .lte);
const all_in_range = try block.addReduce(is_in_range, .And);
break :ok all_in_range;
} else ok: {
const is_in_range = try block.addBinOp(.cmp_lte, diff_unsigned, dest_range);
break :ok is_in_range;
};
// TODO negative_to_unsigned?
try sema.addSafetyCheck(block, src, ok, if (safety_panics_are_enum) .invalid_enum_value else .cast_truncated_data);
} else {
const ok = if (is_vector) ok: {
const is_in_range = try block.addCmpVector(operand, dest_max, .lte);
const all_in_range = try block.addReduce(is_in_range, .And);
break :ok all_in_range;
} else ok: {
const is_in_range = try block.addBinOp(.cmp_lte, operand, dest_max);
break :ok is_in_range;
};
try sema.addSafetyCheck(block, src, ok, if (safety_panics_are_enum) .invalid_enum_value else .cast_truncated_data);
}
} else if (actual_info.signedness == .signed and wanted_info.signedness == .unsigned) {
// no shrinkage, yes sign loss
// requirement: signed to unsigned >= 0
const ok = if (is_vector) ok: {
const scalar_zero = try pt.intValue(operand_scalar_ty, 0);
const zero_val = try sema.splat(operand_ty, scalar_zero);
const zero_inst = Air.internedToRef(zero_val.toIntern());
const is_in_range = try block.addCmpVector(operand, zero_inst, .gte);
const all_in_range = try block.addReduce(is_in_range, .And);
break :ok all_in_range;
} else ok: {
const zero_inst = Air.internedToRef((try pt.intValue(operand_ty, 0)).toIntern());
const is_in_range = try block.addBinOp(.cmp_gte, operand, zero_inst);
break :ok is_in_range;
};
try sema.addSafetyCheck(block, src, ok, if (safety_panics_are_enum) .invalid_enum_value else .negative_to_unsigned);
}
return block.addTyOp(.intcast_safe, dest_ty, operand);
}
return block.addTyOp(.intcast, dest_ty, operand);
}
@ -14316,7 +14226,7 @@ fn zirShl(
}
if (air_tag == .shl_exact) {
const op_ov_tuple_ty = try sema.overflowArithmeticTupleType(lhs_ty);
const op_ov_tuple_ty = try pt.overflowArithmeticTupleType(lhs_ty);
const op_ov = try block.addInst(.{
.tag = .shl_with_overflow,
.data = .{ .ty_pl = .{
@ -16111,7 +16021,7 @@ fn zirOverflowArithmetic(
const maybe_lhs_val = try sema.resolveValue(lhs);
const maybe_rhs_val = try sema.resolveValue(rhs);
const tuple_ty = try sema.overflowArithmeticTupleType(dest_ty);
const tuple_ty = try pt.overflowArithmeticTupleType(dest_ty);
const overflow_ty: Type = .fromInterned(ip.indexToKey(tuple_ty.toIntern()).tuple_type.types.get(ip)[1]);
var result: struct {
@ -16284,24 +16194,6 @@ fn splat(sema: *Sema, ty: Type, val: Value) !Value {
return Value.fromInterned(repeated);
}
fn overflowArithmeticTupleType(sema: *Sema, ty: Type) !Type {
const pt = sema.pt;
const zcu = pt.zcu;
const ip = &zcu.intern_pool;
const ov_ty: Type = if (ty.zigTypeTag(zcu) == .vector) try pt.vectorType(.{
.len = ty.vectorLen(zcu),
.child = .u1_type,
}) else .u1;
const types = [2]InternPool.Index{ ty.toIntern(), ov_ty.toIntern() };
const values = [2]InternPool.Index{ .none, .none };
const tuple_ty = try ip.getTupleType(zcu.gpa, pt.tid, .{
.types = &types,
.values = &values,
});
return .fromInterned(tuple_ty);
}
fn analyzeArithmetic(
sema: *Sema,
block: *Block,
@ -16477,41 +16369,10 @@ fn analyzeArithmetic(
}
if (block.wantSafety() and want_safety and scalar_tag == .int) {
if (zcu.backendSupportsFeature(.safety_checked_instructions)) {
if (air_tag != air_tag_safe) {
_ = try sema.preparePanicId(src, .integer_overflow);
}
return block.addBinOp(air_tag_safe, casted_lhs, casted_rhs);
} else {
const maybe_op_ov: ?Air.Inst.Tag = switch (air_tag) {
.add => .add_with_overflow,
.sub => .sub_with_overflow,
.mul => .mul_with_overflow,
else => null,
};
if (maybe_op_ov) |op_ov_tag| {
const op_ov_tuple_ty = try sema.overflowArithmeticTupleType(resolved_type);
const op_ov = try block.addInst(.{
.tag = op_ov_tag,
.data = .{ .ty_pl = .{
.ty = Air.internedToRef(op_ov_tuple_ty.toIntern()),
.payload = try sema.addExtra(Air.Bin{
.lhs = casted_lhs,
.rhs = casted_rhs,
}),
} },
});
const ov_bit = try sema.tupleFieldValByIndex(block, op_ov, 1, op_ov_tuple_ty);
const any_ov_bit = if (resolved_type.zigTypeTag(zcu) == .vector)
try block.addReduce(ov_bit, .Or)
else
ov_bit;
const no_ov = try block.addBinOp(.cmp_eq, any_ov_bit, .zero_u1);
try sema.addSafetyCheck(block, src, no_ov, .integer_overflow);
return sema.tupleFieldValByIndex(block, op_ov, 0, op_ov_tuple_ty);
}
if (air_tag != air_tag_safe and zcu.backendSupportsFeature(.panic_fn)) {
_ = try sema.preparePanicId(src, .integer_overflow);
}
return block.addBinOp(air_tag_safe, casted_lhs, casted_rhs);
}
return block.addBinOp(air_tag, casted_lhs, casted_rhs);
}

View File

@ -3829,15 +3829,6 @@ pub const Feature = enum {
is_named_enum_value,
error_set_has_value,
field_reordering,
/// When this feature is supported, the backend supports the following AIR instructions:
/// * `Air.Inst.Tag.add_safe`
/// * `Air.Inst.Tag.sub_safe`
/// * `Air.Inst.Tag.mul_safe`
/// * `Air.Inst.Tag.intcast_safe`
/// The motivation for this feature is that it makes AIR smaller, and makes it easier
/// to generate better machine code in the backends. All backends should migrate to
/// enabling this feature.
safety_checked_instructions,
/// If the backend supports running from another thread.
separate_thread,
};

View File

@ -3844,6 +3844,21 @@ pub fn nullValue(pt: Zcu.PerThread, opt_ty: Type) Allocator.Error!Value {
} }));
}
/// `ty` is an integer or a vector of integers.
pub fn overflowArithmeticTupleType(pt: Zcu.PerThread, ty: Type) !Type {
const zcu = pt.zcu;
const ip = &zcu.intern_pool;
const ov_ty: Type = if (ty.zigTypeTag(zcu) == .vector) try pt.vectorType(.{
.len = ty.vectorLen(zcu),
.child = .u1_type,
}) else .u1;
const tuple_ty = try ip.getTupleType(zcu.gpa, pt.tid, .{
.types = &.{ ty.toIntern(), ov_ty.toIntern() },
.values = &.{ .none, .none },
});
return .fromInterned(tuple_ty);
}
pub fn smallestUnsignedInt(pt: Zcu.PerThread, max: u64) Allocator.Error!Type {
return pt.intType(.unsigned, Type.smallestUnsignedBits(max));
}

View File

@ -52,7 +52,12 @@ const Instruction = encoding.Instruction;
const InnerError = CodeGenError || error{OutOfRegisters};
pub fn legalizeFeatures(_: *const std.Target) ?*const Air.Legalize.Features {
return null;
return comptime &.initMany(&.{
.expand_intcast_safe,
.expand_add_safe,
.expand_sub_safe,
.expand_mul_safe,
});
}
pt: Zcu.PerThread,

View File

@ -32,7 +32,12 @@ const compilerRtFloatAbbrev = target_util.compilerRtFloatAbbrev;
const compilerRtIntAbbrev = target_util.compilerRtIntAbbrev;
pub fn legalizeFeatures(_: *const std.Target) ?*const Air.Legalize.Features {
return null;
return comptime &.initMany(&.{
.expand_intcast_safe,
.expand_add_safe,
.expand_sub_safe,
.expand_mul_safe,
});
}
/// Reference to the function declaration the code

View File

@ -88,6 +88,10 @@ pub fn legalizeFeatures(target: *const std.Target) *const Air.Legalize.Features
.unsplat_shift_rhs = false,
.reduce_one_elem_to_bitcast = true,
.expand_intcast_safe = true,
.expand_add_safe = true,
.expand_sub_safe = true,
.expand_mul_safe = true,
}),
};
}

View File

@ -29,7 +29,12 @@ const SpvAssembler = @import("spirv/Assembler.zig");
const InstMap = std.AutoHashMapUnmanaged(Air.Inst.Index, IdRef);
pub fn legalizeFeatures(_: *const std.Target) ?*const Air.Legalize.Features {
return null;
return comptime &.initMany(&.{
.expand_intcast_safe,
.expand_add_safe,
.expand_sub_safe,
.expand_mul_safe,
});
}
pub const zig_call_abi_ver = 3;

View File

@ -842,10 +842,6 @@ pub inline fn backendSupportsFeature(backend: std.builtin.CompilerBackend, compt
.stage2_c, .stage2_llvm, .stage2_x86_64 => true,
else => false,
},
.safety_checked_instructions => switch (backend) {
.stage2_llvm => true,
else => false,
},
.separate_thread => switch (backend) {
.stage2_llvm => false,
else => true,