authorgravatar for git@vexu.euVeikka Tuominen <git@vexu.eu> 2022-12-18 01:57:49+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2022-12-18 01:57:49+02:00
log901c3e96368bd9fa9ee9858c0295ee05ef1d7146
treeb5a12345800be59ca68070ce1eff68479c98fff8
parent9cc49548aa3f98b40a3d7a9cbc12d55ae7a9a298
parent35750cd54f41477f25c77b8c44a220b57b40fd60
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #13552 from hryx/comparus-tautologicus

Sema: elide integer comparisons with guaranteed outcomes

6 files changed, 267 insertions(+), 14 deletions(-)

lib/std/math.zig+22
...@@ -1437,6 +1437,19 @@ pub const CompareOperator = enum {...@@ -1437,6 +1437,19 @@ pub const CompareOperator = enum {
1437 gt,1437 gt,
1438 /// Not equal (`!=`)1438 /// Not equal (`!=`)
1439 neq,1439 neq,
1440
1441 /// Reverse the direction of the comparison.
1442 /// Use when swapping the left and right hand operands.
1443 pub fn reverse(op: CompareOperator) CompareOperator {
1444 return switch (op) {
1445 .lt => .gt,
1446 .lte => .gte,
1447 .gt => .lt,
1448 .gte => .lte,
1449 .eq => .eq,
1450 .neq => .neq,
1451 };
1452 }
1440};1453};
14411454
1442/// This function does the same thing as comparison operators, however the1455/// This function does the same thing as comparison operators, however the
...@@ -1496,6 +1509,15 @@ test "order.compare" {...@@ -1496,6 +1509,15 @@ test "order.compare" {
1496 try testing.expect(order(1, 0).compare(.neq));1509 try testing.expect(order(1, 0).compare(.neq));
1497}1510}
14981511
1512test "compare.reverse" {
1513 inline for (@typeInfo(CompareOperator).Enum.fields) |op_field| {
1514 const op = @intToEnum(CompareOperator, op_field.value);
1515 try testing.expect(compare(2, op, 3) == compare(3, op.reverse(), 2));
1516 try testing.expect(compare(3, op, 3) == compare(3, op.reverse(), 3));
1517 try testing.expect(compare(4, op, 3) == compare(3, op.reverse(), 4));
1518 }
1519}
1520
1499/// Returns a mask of all ones if value is true,1521/// Returns a mask of all ones if value is true,
1500/// and a mask of all zeroes if value is false.1522/// and a mask of all zeroes if value is false.
1501/// Compiles to one instruction for register sized integers.1523/// Compiles to one instruction for register sized integers.
src/Sema.zig+130
...@@ -28497,6 +28497,19 @@ fn cmpNumeric(...@@ -28497,6 +28497,19 @@ fn cmpNumeric(
28497 const runtime_src: LazySrcLoc = src: {28497 const runtime_src: LazySrcLoc = src: {
28498 if (try sema.resolveMaybeUndefVal(lhs)) |lhs_val| {28498 if (try sema.resolveMaybeUndefVal(lhs)) |lhs_val| {
28499 if (try sema.resolveMaybeUndefVal(rhs)) |rhs_val| {28499 if (try sema.resolveMaybeUndefVal(rhs)) |rhs_val| {
28500 // Compare ints: const vs. undefined (or vice versa)
28501 if (!lhs_val.isUndef() and (lhs_ty.isInt() or lhs_ty_tag == .ComptimeInt) and rhs_ty.isInt() and rhs_val.isUndef()) {
28502 try sema.resolveLazyValue(lhs_val);
28503 if (sema.compareIntsOnlyPossibleResult(target, lhs_val, op, rhs_ty)) |res| {
28504 return if (res) Air.Inst.Ref.bool_true else Air.Inst.Ref.bool_false;
28505 }
28506 } else if (!rhs_val.isUndef() and (rhs_ty.isInt() or rhs_ty_tag == .ComptimeInt) and lhs_ty.isInt() and lhs_val.isUndef()) {
28507 try sema.resolveLazyValue(rhs_val);
28508 if (sema.compareIntsOnlyPossibleResult(target, rhs_val, op.reverse(), lhs_ty)) |res| {
28509 return if (res) Air.Inst.Ref.bool_true else Air.Inst.Ref.bool_false;
28510 }
28511 }
28512
28500 if (lhs_val.isUndef() or rhs_val.isUndef()) {28513 if (lhs_val.isUndef() or rhs_val.isUndef()) {
28501 return sema.addConstUndef(Type.bool);28514 return sema.addConstUndef(Type.bool);
28502 }28515 }
...@@ -28513,9 +28526,25 @@ fn cmpNumeric(...@@ -28513,9 +28526,25 @@ fn cmpNumeric(
28513 return Air.Inst.Ref.bool_false;28526 return Air.Inst.Ref.bool_false;
28514 }28527 }
28515 } else {28528 } else {
28529 if (!lhs_val.isUndef() and (lhs_ty.isInt() or lhs_ty_tag == .ComptimeInt) and rhs_ty.isInt()) {
28530 // Compare ints: const vs. var
28531 try sema.resolveLazyValue(lhs_val);
28532 if (sema.compareIntsOnlyPossibleResult(target, lhs_val, op, rhs_ty)) |res| {
28533 return if (res) Air.Inst.Ref.bool_true else Air.Inst.Ref.bool_false;
28534 }
28535 }
28516 break :src rhs_src;28536 break :src rhs_src;
28517 }28537 }
28518 } else {28538 } else {
28539 if (try sema.resolveMaybeUndefVal(rhs)) |rhs_val| {
28540 if (!rhs_val.isUndef() and (rhs_ty.isInt() or rhs_ty_tag == .ComptimeInt) and lhs_ty.isInt()) {
28541 // Compare ints: var vs. const
28542 try sema.resolveLazyValue(rhs_val);
28543 if (sema.compareIntsOnlyPossibleResult(target, rhs_val, op.reverse(), lhs_ty)) |res| {
28544 return if (res) Air.Inst.Ref.bool_true else Air.Inst.Ref.bool_false;
28545 }
28546 }
28547 }
28519 break :src lhs_src;28548 break :src lhs_src;
28520 }28549 }
28521 };28550 };
...@@ -28704,6 +28733,107 @@ fn cmpNumeric(...@@ -28704,6 +28733,107 @@ fn cmpNumeric(
28704 return block.addBinOp(Air.Inst.Tag.fromCmpOp(op, block.float_mode == .Optimized), casted_lhs, casted_rhs);28733 return block.addBinOp(Air.Inst.Tag.fromCmpOp(op, block.float_mode == .Optimized), casted_lhs, casted_rhs);
28705}28734}
2870628735
28736/// Asserts that LHS value is an int or comptime int and not undefined, and that RHS type is an int.
28737/// Given a const LHS and an unknown RHS, attempt to determine whether `op` has a guaranteed result.
28738/// If it cannot be determined, returns null.
28739/// Otherwise returns a bool for the guaranteed comparison operation.
28740fn compareIntsOnlyPossibleResult(sema: *Sema, target: std.Target, lhs_val: Value, op: std.math.CompareOperator, rhs_ty: Type) ?bool {
28741 const rhs_info = rhs_ty.intInfo(target);
28742 const vs_zero = lhs_val.orderAgainstZeroAdvanced(sema) catch unreachable;
28743 const is_zero = vs_zero == .eq;
28744 const is_negative = vs_zero == .lt;
28745 const is_positive = vs_zero == .gt;
28746
28747 // Anything vs. zero-sized type has guaranteed outcome.
28748 if (rhs_info.bits == 0) return switch (op) {
28749 .eq, .lte, .gte => is_zero,
28750 .neq, .lt, .gt => !is_zero,
28751 };
28752
28753 // Special case for i1, which can only be 0 or -1.
28754 // Zero and positive ints have guaranteed outcome.
28755 if (rhs_info.bits == 1 and rhs_info.signedness == .signed) {
28756 if (is_positive) return switch (op) {
28757 .gt, .gte, .neq => true,
28758 .lt, .lte, .eq => false,
28759 };
28760 if (is_zero) return switch (op) {
28761 .gte => true,
28762 .lt => false,
28763 .gt, .lte, .eq, .neq => null,
28764 };
28765 }
28766
28767 // Negative vs. unsigned has guaranteed outcome.
28768 if (rhs_info.signedness == .unsigned and is_negative) return switch (op) {
28769 .eq, .gt, .gte => false,
28770 .neq, .lt, .lte => true,
28771 };
28772
28773 const sign_adj = @boolToInt(!is_negative and rhs_info.signedness == .signed);
28774 const req_bits = lhs_val.intBitCountTwosComp(target) + sign_adj;
28775
28776 // No sized type can have more than 65535 bits.
28777 // The RHS type operand is either a runtime value or sized (but undefined) constant.
28778 if (req_bits > 65535) return switch (op) {
28779 .lt, .lte => is_negative,
28780 .gt, .gte => is_positive,
28781 .eq => false,
28782 .neq => true,
28783 };
28784 const fits = req_bits <= rhs_info.bits;
28785
28786 // Oversized int has guaranteed outcome.
28787 switch (op) {
28788 .eq => return if (!fits) false else null,
28789 .neq => return if (!fits) true else null,
28790 .lt, .lte => if (!fits) return is_negative,
28791 .gt, .gte => if (!fits) return !is_negative,
28792 }
28793
28794 // For any other comparison, we need to know if the LHS value is
28795 // equal to the maximum or minimum possible value of the RHS type.
28796 const edge: struct { min: bool, max: bool } = edge: {
28797 if (is_zero and rhs_info.signedness == .unsigned) break :edge .{
28798 .min = true,
28799 .max = false,
28800 };
28801
28802 if (req_bits != rhs_info.bits) break :edge .{
28803 .min = false,
28804 .max = false,
28805 };
28806
28807 var ty_buffer: Type.Payload.Bits = .{
28808 .base = .{ .tag = if (is_negative) .int_signed else .int_unsigned },
28809 .data = @intCast(u16, req_bits),
28810 };
28811 const ty = Type.initPayload(&ty_buffer.base);
28812 const pop_count = lhs_val.popCount(ty, target);
28813
28814 if (is_negative) {
28815 break :edge .{
28816 .min = pop_count == 1,
28817 .max = false,
28818 };
28819 } else {
28820 break :edge .{
28821 .min = false,
28822 .max = pop_count == req_bits - sign_adj,
28823 };
28824 }
28825 };
28826
28827 assert(fits);
28828 return switch (op) {
28829 .lt => if (edge.max) false else null,
28830 .lte => if (edge.min) true else null,
28831 .gt => if (edge.min) false else null,
28832 .gte => if (edge.max) true else null,
28833 .eq, .neq => unreachable,
28834 };
28835}
28836
28707/// Asserts that lhs and rhs types are both vectors.28837/// Asserts that lhs and rhs types are both vectors.
28708fn cmpVector(28838fn cmpVector(
28709 sema: *Sema,28839 sema: *Sema,
src/test.zig+4-2
...@@ -1723,7 +1723,8 @@ pub const TestContext = struct {...@@ -1723,7 +1723,8 @@ pub const TestContext = struct {
1723 (case_msg.src.column == std.math.maxInt(u32) or1723 (case_msg.src.column == std.math.maxInt(u32) or
1724 actual_msg.column == case_msg.src.column) and1724 actual_msg.column == case_msg.src.column) and
1725 std.mem.eql(u8, expected_msg, actual_msg.msg) and1725 std.mem.eql(u8, expected_msg, actual_msg.msg) and
1726 case_msg.src.kind == .note)1726 case_msg.src.kind == .note and
1727 actual_msg.count == case_msg.src.count)
1727 {1728 {
1728 handled_errors[i] = true;1729 handled_errors[i] = true;
1729 break;1730 break;
...@@ -1733,7 +1734,8 @@ pub const TestContext = struct {...@@ -1733,7 +1734,8 @@ pub const TestContext = struct {
1733 if (ex_tag != .plain) continue;1734 if (ex_tag != .plain) continue;
17341735
1735 if (std.mem.eql(u8, case_msg.plain.msg, plain.msg) and1736 if (std.mem.eql(u8, case_msg.plain.msg, plain.msg) and
1736 case_msg.plain.kind == .note)1737 case_msg.plain.kind == .note and
1738 case_msg.plain.count == plain.count)
1737 {1739 {
1738 handled_errors[i] = true;1740 handled_errors[i] = true;
1739 break;1741 break;
src/value.zig+2-11
...@@ -1756,17 +1756,8 @@ pub const Value = extern union {...@@ -1756,17 +1756,8 @@ pub const Value = extern union {
1756 const info = ty.intInfo(target);1756 const info = ty.intInfo(target);
17571757
1758 var buffer: Value.BigIntSpace = undefined;1758 var buffer: Value.BigIntSpace = undefined;
1759 const operand_bigint = val.toBigInt(&buffer, target);1759 const int = val.toBigInt(&buffer, target);
17601760 return @intCast(u64, int.popCount(info.bits));
1761 var limbs_buffer: [4]std.math.big.Limb = undefined;
1762 var result_bigint = BigIntMutable{
1763 .limbs = &limbs_buffer,
1764 .positive = undefined,
1765 .len = undefined,
1766 };
1767 result_bigint.popCount(operand_bigint, info.bits);
1768
1769 return result_bigint.toConst().to(u64) catch unreachable;
1770 },1761 },
1771 }1762 }
1772 }1763 }
test/behavior.zig+1-1
...@@ -160,7 +160,7 @@ test {...@@ -160,7 +160,7 @@ test {
160 _ = @import("behavior/incomplete_struct_param_tld.zig");160 _ = @import("behavior/incomplete_struct_param_tld.zig");
161 _ = @import("behavior/inline_switch.zig");161 _ = @import("behavior/inline_switch.zig");
162 _ = @import("behavior/int128.zig");162 _ = @import("behavior/int128.zig");
163 _ = @import("behavior/int_div.zig");163 _ = @import("behavior/int_comparison_elision.zig");
164 _ = @import("behavior/inttoptr.zig");164 _ = @import("behavior/inttoptr.zig");
165 _ = @import("behavior/ir_block_deps.zig");165 _ = @import("behavior/ir_block_deps.zig");
166 _ = @import("behavior/lower_strlit_to_vector.zig");166 _ = @import("behavior/lower_strlit_to_vector.zig");
test/behavior/int_comparison_elision.zig created+108
...@@ -0,0 +1,108 @@
1const std = @import("std");
2const minInt = std.math.minInt;
3const maxInt = std.math.maxInt;
4const builtin = @import("builtin");
5
6test "int comparison elision" {
7 testIntEdges(u0);
8 testIntEdges(i0);
9 testIntEdges(u1);
10 testIntEdges(i1);
11 testIntEdges(u4);
12 testIntEdges(i4);
13
14 // TODO: support int types > 128 bits wide in other backends
15 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
16 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
17 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
18 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
19 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
20
21 // TODO: panic: integer overflow with int types > 65528 bits wide
22 // TODO: LLVM generates too many parameters for wasmtime when splitting up int > 64000 bits wide
23 testIntEdges(u64000);
24 testIntEdges(i64000);
25}
26
27// All comparisons in this test have a guaranteed result,
28// so one branch of each 'if' should never be analyzed.
29fn testIntEdges(comptime T: type) void {
30 const min = minInt(T);
31 const max = maxInt(T);
32
33 var runtime_val: T = undefined;
34
35 if (min > runtime_val) @compileError("analyzed impossible branch");
36 if (min <= runtime_val) {} else @compileError("analyzed impossible branch");
37 if (runtime_val < min) @compileError("analyzed impossible branch");
38 if (runtime_val >= min) {} else @compileError("analyzed impossible branch");
39
40 if (min - 1 > runtime_val) @compileError("analyzed impossible branch");
41 if (min - 1 >= runtime_val) @compileError("analyzed impossible branch");
42 if (min - 1 < runtime_val) {} else @compileError("analyzed impossible branch");
43 if (min - 1 <= runtime_val) {} else @compileError("analyzed impossible branch");
44 if (min - 1 == runtime_val) @compileError("analyzed impossible branch");
45 if (min - 1 != runtime_val) {} else @compileError("analyzed impossible branch");
46 if (runtime_val < min - 1) @compileError("analyzed impossible branch");
47 if (runtime_val <= min - 1) @compileError("analyzed impossible branch");
48 if (runtime_val > min - 1) {} else @compileError("analyzed impossible branch");
49 if (runtime_val >= min - 1) {} else @compileError("analyzed impossible branch");
50 if (runtime_val == min - 1) @compileError("analyzed impossible branch");
51 if (runtime_val != min - 1) {} else @compileError("analyzed impossible branch");
52
53 if (max >= runtime_val) {} else @compileError("analyzed impossible branch");
54 if (max < runtime_val) @compileError("analyzed impossible branch");
55 if (runtime_val <= max) {} else @compileError("analyzed impossible branch");
56 if (runtime_val > max) @compileError("analyzed impossible branch");
57
58 if (max + 1 > runtime_val) {} else @compileError("analyzed impossible branch");
59 if (max + 1 >= runtime_val) {} else @compileError("analyzed impossible branch");
60 if (max + 1 < runtime_val) @compileError("analyzed impossible branch");
61 if (max + 1 <= runtime_val) @compileError("analyzed impossible branch");
62 if (max + 1 == runtime_val) @compileError("analyzed impossible branch");
63 if (max + 1 != runtime_val) {} else @compileError("analyzed impossible branch");
64 if (runtime_val < max + 1) {} else @compileError("analyzed impossible branch");
65 if (runtime_val <= max + 1) {} else @compileError("analyzed impossible branch");
66 if (runtime_val > max + 1) @compileError("analyzed impossible branch");
67 if (runtime_val >= max + 1) @compileError("analyzed impossible branch");
68 if (runtime_val == max + 1) @compileError("analyzed impossible branch");
69 if (runtime_val != max + 1) {} else @compileError("analyzed impossible branch");
70
71 const undef_const: T = undefined;
72
73 if (min > undef_const) @compileError("analyzed impossible branch");
74 if (min <= undef_const) {} else @compileError("analyzed impossible branch");
75 if (undef_const < min) @compileError("analyzed impossible branch");
76 if (undef_const >= min) {} else @compileError("analyzed impossible branch");
77
78 if (min - 1 > undef_const) @compileError("analyzed impossible branch");
79 if (min - 1 >= undef_const) @compileError("analyzed impossible branch");
80 if (min - 1 < undef_const) {} else @compileError("analyzed impossible branch");
81 if (min - 1 <= undef_const) {} else @compileError("analyzed impossible branch");
82 if (min - 1 == undef_const) @compileError("analyzed impossible branch");
83 if (min - 1 != undef_const) {} else @compileError("analyzed impossible branch");
84 if (undef_const < min - 1) @compileError("analyzed impossible branch");
85 if (undef_const <= min - 1) @compileError("analyzed impossible branch");
86 if (undef_const > min - 1) {} else @compileError("analyzed impossible branch");
87 if (undef_const >= min - 1) {} else @compileError("analyzed impossible branch");
88 if (undef_const == min - 1) @compileError("analyzed impossible branch");
89 if (undef_const != min - 1) {} else @compileError("analyzed impossible branch");
90
91 if (max >= undef_const) {} else @compileError("analyzed impossible branch");
92 if (max < undef_const) @compileError("analyzed impossible branch");
93 if (undef_const <= max) {} else @compileError("analyzed impossible branch");
94 if (undef_const > max) @compileError("analyzed impossible branch");
95
96 if (max + 1 > undef_const) {} else @compileError("analyzed impossible branch");
97 if (max + 1 >= undef_const) {} else @compileError("analyzed impossible branch");
98 if (max + 1 < undef_const) @compileError("analyzed impossible branch");
99 if (max + 1 <= undef_const) @compileError("analyzed impossible branch");
100 if (max + 1 == undef_const) @compileError("analyzed impossible branch");
101 if (max + 1 != undef_const) {} else @compileError("analyzed impossible branch");
102 if (undef_const < max + 1) {} else @compileError("analyzed impossible branch");
103 if (undef_const <= max + 1) {} else @compileError("analyzed impossible branch");
104 if (undef_const > max + 1) @compileError("analyzed impossible branch");
105 if (undef_const >= max + 1) @compileError("analyzed impossible branch");
106 if (undef_const == max + 1) @compileError("analyzed impossible branch");
107 if (undef_const != max + 1) {} else @compileError("analyzed impossible branch");
108}