| author | |
| committer | |
| log | 901c3e96368bd9fa9ee9858c0295ee05ef1d7146 |
| tree | b5a12345800be59ca68070ce1eff68479c98fff8 |
| parent | 9cc49548aa3f98b40a3d7a9cbc12d55ae7a9a298 |
| parent | 35750cd54f41477f25c77b8c44a220b57b40fd60 |
| signature |
Sema: elide integer comparisons with guaranteed outcomes6 files changed, 267 insertions(+), 14 deletions(-)
lib/std/math.zig+22| ... | ... | @@ -1437,6 +1437,19 @@ pub const CompareOperator = enum { |
| 1437 | 1437 | gt, |
| 1438 | 1438 | /// Not equal (`!=`) |
| 1439 | 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 | }; |
| 1441 | 1454 | |
| 1442 | 1455 | /// This function does the same thing as comparison operators, however the |
| ... | ... | @@ -1496,6 +1509,15 @@ test "order.compare" { |
| 1496 | 1509 | try testing.expect(order(1, 0).compare(.neq)); |
| 1497 | 1510 | } |
| 1498 | 1511 | |
| 1512 | test "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 | 1521 | /// Returns a mask of all ones if value is true, |
| 1500 | 1522 | /// and a mask of all zeroes if value is false. |
| 1501 | 1523 | /// Compiles to one instruction for register sized integers. |
src/Sema.zig+130| ... | ... | @@ -28497,6 +28497,19 @@ fn cmpNumeric( |
| 28497 | 28497 | const runtime_src: LazySrcLoc = src: { |
| 28498 | 28498 | if (try sema.resolveMaybeUndefVal(lhs)) |lhs_val| { |
| 28499 | 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 | 28513 | if (lhs_val.isUndef() or rhs_val.isUndef()) { |
| 28501 | 28514 | return sema.addConstUndef(Type.bool); |
| 28502 | 28515 | } |
| ... | ... | @@ -28513,9 +28526,25 @@ fn cmpNumeric( |
| 28513 | 28526 | return Air.Inst.Ref.bool_false; |
| 28514 | 28527 | } |
| 28515 | 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 | 28536 | break :src rhs_src; |
| 28517 | 28537 | } |
| 28518 | 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 | 28548 | break :src lhs_src; |
| 28520 | 28549 | } |
| 28521 | 28550 | }; |
| ... | ... | @@ -28704,6 +28733,107 @@ fn cmpNumeric( |
| 28704 | 28733 | return block.addBinOp(Air.Inst.Tag.fromCmpOp(op, block.float_mode == .Optimized), casted_lhs, casted_rhs); |
| 28705 | 28734 | } |
| 28706 | 28735 | |
| 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. | |
| 28740 | fn 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 | 28837 | /// Asserts that lhs and rhs types are both vectors. |
| 28708 | 28838 | fn cmpVector( |
| 28709 | 28839 | sema: *Sema, |
src/test.zig+4-2| ... | ... | @@ -1723,7 +1723,8 @@ pub const TestContext = struct { |
| 1723 | 1723 | (case_msg.src.column == std.math.maxInt(u32) or |
| 1724 | 1724 | actual_msg.column == case_msg.src.column) and |
| 1725 | 1725 | 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 | 1729 | handled_errors[i] = true; |
| 1729 | 1730 | break; |
| ... | ... | @@ -1733,7 +1734,8 @@ pub const TestContext = struct { |
| 1733 | 1734 | if (ex_tag != .plain) continue; |
| 1734 | 1735 | |
| 1735 | 1736 | 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 | 1740 | handled_errors[i] = true; |
| 1739 | 1741 | break; |
src/value.zig+2-11| ... | ... | @@ -1756,17 +1756,8 @@ pub const Value = extern union { |
| 1756 | 1756 | const info = ty.intInfo(target); |
| 1757 | 1757 | |
| 1758 | 1758 | var buffer: Value.BigIntSpace = undefined; |
| 1759 | const operand_bigint = val.toBigInt(&buffer, target); | |
| 1760 | ||
| 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; | |
| 1759 | const int = val.toBigInt(&buffer, target); | |
| 1760 | return @intCast(u64, int.popCount(info.bits)); | |
| 1770 | 1761 | }, |
| 1771 | 1762 | } |
| 1772 | 1763 | } |
test/behavior.zig+1-1| ... | ... | @@ -160,7 +160,7 @@ test { |
| 160 | 160 | _ = @import("behavior/incomplete_struct_param_tld.zig"); |
| 161 | 161 | _ = @import("behavior/inline_switch.zig"); |
| 162 | 162 | _ = @import("behavior/int128.zig"); |
| 163 | _ = @import("behavior/int_div.zig"); | |
| 163 | _ = @import("behavior/int_comparison_elision.zig"); | |
| 164 | 164 | _ = @import("behavior/inttoptr.zig"); |
| 165 | 165 | _ = @import("behavior/ir_block_deps.zig"); |
| 166 | 166 | _ = @import("behavior/lower_strlit_to_vector.zig"); |
test/behavior/int_comparison_elision.zig created+108| ... | ... | @@ -0,0 +1,108 @@ |
| 1 | const std = @import("std"); | |
| 2 | const minInt = std.math.minInt; | |
| 3 | const maxInt = std.math.maxInt; | |
| 4 | const builtin = @import("builtin"); | |
| 5 | ||
| 6 | test "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. | |
| 29 | fn 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 | } |