/** This module defines facilities for efficient checking of integral operations against overflow, casting with loss of precision, unexpected change of sign, etc. The checking (and possibly correction) can be done at operation level, for example $(D opChecked!"+"(x, y, overflow)) adds two integrals `x` and `y` and sets `overflow` to `true` if an overflow occurred. The flag (passed by reference) is not touched if the operation succeeded, so the same flag can be reused for a sequence of operations and tested at the end. Issuing individual checked operations is flexible and efficient but often tedious. The `Checked` facility offers encapsulated integral wrappers that do all checking internally and have configurable behavior upon erroneous results. For example, `Checked!int` is a type that behaves like `int` but issues an `assert(0)` whenever involved in an operation that produces the arithmetically wrong result. For example $(D Checked!int(1_000_000) * 10_000) fails with `assert(0)` because the operation overflows. Also, $(D Checked!int(-1) > uint(0)) fails with `assert(0)` (even though the built-in comparison $(D int(-1) > uint(0)) is surprisingly true due to language's conversion rules modeled after C). Thus, `Checked!int` is a virtually drop-in replacement for `int` useable in debug builds, to be replaced by `int` if efficiency demands it. `Checked` has customizable behavior with the help of a second type parameter, `Hook`. Depending on what methods `Hook` defines, core operations on the underlying integral may be verified for overflow or completely redefined. If `Hook` defines no method at all and carries no state, there is no change in behavior, i.e. $(D Checked!(int, void)) is a wrapper around `int` that adds no customization at all. This module provides a few predefined hooks (below) that add useful behavior to `Checked`: $(UL $(LI `Croak` fails every incorrect operation with `assert(0)`. It is the default second parameter, i.e. `Checked!short` is the same as $(D Checked!(short, Croak)).) $(LI `ProperCompare` fixes the comparison operators `==`, `!=`, `<`, `<=`, `>`, and `>=` to return correct results in all circumstances, at a slight cost in efficiency. For example, $(D Checked!(uint, ProperCompare)(1) > -1) is `true`, which is not the case with the built-in comparison. Also, comparing numbers for equality with floating-point numbers only passes if the integral can be converted to the floating-point number precisely, so as to preserve transitivity of equality.) $(LI `WithNaN` reserves a special "Not a Number" value. ) ) These policies may be used alone, e.g. $(D Checked!(uint, WithNaN)) defines a `uint`-like type that reaches a stable NaN state for all erroneous operations. They may also be "stacked" on top of each other, owing to the property that a checked integral emulates an actual integral, which means another checked integral can be built on top of it. Some interesting combinations include: $(UL $(LI $(D Checked!(Checked!int, ProperCompare)) defines an `int` with fixed comparison operators that will fail with `assert(0)` upon overflow. (Recall that `Croak` is the default policy.) The order in which policies are combined is important because the outermost policy (`ProperCompare` in this case) has the first crack at intercepting an operator. The converse combination $(D Checked!(Checked!(int, ProperCompare))) is meaningless because `Croak` will intercept comparison and will fail without giving `ProperCompare` a chance to intervene.) $(LI $(D Checked!(Checked!(int, ProperCompare), WithNaN)) defines an `int`-like type that supports a NaN value. For values that are not NaN, comparison works properly. Again the composition order is important; $(D Checked!(Checked!(int, WithNaN), ProperCompare)) does not have good semantics because `ProperCompare` intercepts comparisons before the numbers involved are tested for NaN.) ) */ module std.experimental.checkedint; import std.traits : isFloatingPoint, isIntegral, isNumeric, isUnsigned, Unqual; import std.conv : unsigned; /** Checked integral type wraps an integral `T` and customizes its behavior with the help of a `Hook` type. The type wrapped must be one of the predefined integrals (unqualified), or another instance of `Checked`. */ struct Checked(T, Hook = Croak) if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) { import std.algorithm : among; import std.traits : hasMember; import std.experimental.allocator.common : stateSize; /** The type of the integral subject to checking. */ alias Payload = T; // state { static if (hasMember!(Hook, "defaultValue")) private T payload = Hook.defaultValue!T; else private T payload; /** `hook` is a member variable if it has state, or an alias for `Hook` otherwise. */ static if (stateSize!Hook > 0) Hook hook; else alias hook = Hook; // } state // representation /** Returns a copy of the underlying value. */ auto representation() inout { return payload; } /// unittest { auto x = Checked!ubyte(ubyte(42)); static assert(is(typeof(x.representation()) == ubyte)); assert(x.representation == 42); } /** Defines the minimum and maximum allowed. */ static if (hasMember!(Hook, "min")) enum min = Checked!(T, Hook)(Hook.min!T); else enum min = Checked(T.min); /// ditto static if (hasMember!(Hook, "max")) enum max = Checked(Hook.max!T); else enum max = Checked(T.max); /// unittest { assert(Checked!short.min == -32768); assert(Checked!(short, WithNaN).min == -32767); assert(Checked!(uint, WithNaN).max == uint.max - 1); } /** Constructor taking a value properly convertible to the underlying type. `U` may be either an integral that can be converted to `T` without a loss, or another `Checked` instance whose payload may be in turn converted to `T` without a loss. */ this(U)(U rhs) if (valueConvertible!(U, T) || !isIntegral!T && is(typeof(T(rhs))) || is(U == Checked!(V, W), V, W) && is(typeof(Checked(rhs.payload)))) { static if (isIntegral!U) payload = rhs; else payload = rhs.payload; } /** Assignment operator. Has the same constraints as the constructor. */ void opAssign(U)(U rhs) if (is(typeof(Checked(rhs)))) { static if (isIntegral!U) payload = rhs; else payload = rhs.payload; } // opCast /** Casting operator to integral, `bool`, or floating point type. If `Hook` defines `hookOpCast`, the call immediately returns `hook.hookOpCast!U(representation)`. Otherwise, casting to `bool` yields $(D representation != 0) and casting to another integral that can represent all values of `T` returns `representation` promoted to `U`. If a cast to a floating-point type is requested and `Hook` defines `onBadCast`, the cast is verified by ensuring $(D representation == cast(T) U(representation)). If that is not `true`, `hook.onBadCast!U(payload)` is returned. If a cast to an integral type is requested and `Hook` defines `onBadCast`, the cast is verified by ensuring `representation` and $(D cast(U) representation) are the same arithmetic number. (Note that `int(-1)` and `uint(1)` are different values arithmetically although they have the same bitwise representation and compare equal by language rules.) If the numbers are not arithmetically equal, `hook.onBadCast!U(payload)` is returned. */ U opCast(U)() if (isIntegral!U || isFloatingPoint!U || is(U == bool)) { static if (hasMember!(Hook, "hookOpCast")) { return hook.hookOpCast!U(payload); } else static if (is(U == bool)) { return payload != 0; } else static if (valueConvertible!(T, U)) { return payload; } // may lose bits or precision else static if (!hasMember!(Hook, "onBadCast")) { return cast(U) payload; } else { if (isUnsigned!T || !isUnsigned!U || T.sizeof > U.sizeof || payload >= 0) { auto result = cast(U) payload; // If signedness is different, we need additional checks if (result == payload && (!isUnsigned!T || isUnsigned!U || result >= 0)) return result; } return hook.onBadCast!U(payload); } } /// unittest { assert(cast(uint) Checked!int(42) == 42); assert(cast(uint) Checked!(int, WithNaN)(-42) == uint.max); } // opEquals /** Compares `this` against `rhs` for equality. If `Hook` defines `hookOpEquals`, the function forwards to $(D hook.hookOpEquals(representation, rhs)). Otherwise, the result of the built-in operation $(D payload == rhs) is returned. If `U` is an instance of `Checked` */ bool opEquals(U)(U rhs) if (isIntegral!U || isFloatingPoint!U || is(U == bool) || is(U == Checked!(V, W), V, W) && is(typeof(this == rhs.payload))) { static if (is(U == Checked!(V, W), V, W)) { alias R = typeof(payload + rhs.payload); static if (is(Hook == W)) { // Use the lhs hook if there return this == rhs.payload; } else static if (valueConvertible!(T, R) && valueConvertible!(V, R)) { return payload == rhs.payload; } else static if (hasMember!(Hook, "hookOpEquals")) { return hook.hookOpEquals(payload, rhs.payload); } else static if (hasMember!(Hook1, "hookOpEquals")) { return rhs.hook.hookOpEquals(rhs.payload, payload); } else { return payload == rhs.payload; } } else static if (hasMember!(Hook, "hookOpEquals")) return hook.hookOpEquals(payload, rhs); else static if (isIntegral!U || isFloatingPoint!U || is(U == bool)) return payload == rhs; } // opCmp /** */ auto opCmp(U)(const U rhs) //const pure @safe nothrow @nogc if (isIntegral!U || isFloatingPoint!U || is(U == bool)) { static if (hasMember!(Hook, "hookOpCmp")) { return hook.hookOpCmp(payload, rhs); } else static if (valueConvertible!(T, U) || valueConvertible!(U, T)) { return payload < rhs ? -1 : payload > rhs; } else static if (isFloatingPoint!U) { U lhs = payload; return lhs < rhs ? U(-1.0) : lhs > rhs ? U(1.0) : lhs == rhs ? U(0.0) : U.init; } else { return payload < rhs ? -1 : payload > rhs; } } /// ditto auto opCmp(U, Hook1)(Checked!(U, Hook1) rhs) { alias R = typeof(payload + rhs.payload); static if (valueConvertible!(T, R) && valueConvertible!(T, R)) { return payload < rhs.payload ? -1 : payload > rhs.payload; } else static if (is(Hook == Hook1)) { // Use the lhs hook return this.opCmp(rhs.payload); } else static if (hasMember!(Hook, "hookOpCmp")) { return hook.hookOpCmp(payload, rhs); } else static if (hasMember!(Hook1, "hookOpCmp")) { return rhs.hook.hookOpCmp(rhs.payload, this); } else { return payload < rhs.payload ? -1 : payload > rhs.payload; } } // opUnary /** */ auto opUnary(string op)() if (op == "+" || op == "-" || op == "~") { static if (op == "+") return Checked(this); // "+" is not hookable else static if (hasMember!(Hook, "hookOpUnary")) { auto r = hook.hookOpUnary!op(payload); return Checked!(typeof(r), Hook)(r); } else static if (!isUnsigned!T && op == "-" && hasMember!(Hook, "onOverflow")) { import core.checkedint; alias R = typeof(-payload); bool overflow; auto r = negs(R(payload), overflow); if (overflow) r = hook.onOverflow!op(payload); return Checked(r); } else return Checked(mixin(op ~ "payload")); } /// ditto ref Checked opUnary(string op)() return if (op == "++" || op == "--") { static if (hasMember!(Hook, "hookOpUnary")) hook.hookOpUnary!op(payload); else static if (hasMember!(Hook, "onOverflow")) { static if (op == "++") { if (payload == max.payload) payload = hook.onOverflow!"++"(payload); else ++payload; } else { if (payload == min.payload) payload = hook.onOverflow!"--"(payload); else --payload; } } else mixin(op ~ "payload;"); return this; } // opBinary /** */ auto opBinary(string op, Rhs)(const Rhs rhs) if (isIntegral!Rhs || isFloatingPoint!Rhs || is(Rhs == bool)) { alias R = typeof(payload + rhs); static assert(is(typeof(mixin("payload"~op~"rhs")) == R)); static if (isIntegral!R) alias Result = Checked!(R, Hook); else alias Result = R; static if (hasMember!(Hook, "hookOpBinary")) { auto r = hook.hookOpBinary!op(payload, rhs); return Checked!(typeof(r), Hook)(r); } else static if (is(Rhs == bool)) { return mixin("this"~op~"ubyte(rhs)"); } else static if (isFloatingPoint!Rhs) { return mixin("payload"~op~"rhs"); } else static if (hasMember!(Hook, "onOverflow")) { bool overflow; auto r = opChecked!op(payload, rhs, overflow); if (overflow) r = hook.onOverflow!op(payload, rhs); return Result(r); } else { // Default is built-in behavior return Result(mixin("payload"~op~"rhs")); } } /// ditto auto opBinary(string op, U, Hook1)(Checked!(U, Hook1) rhs) { alias R = typeof(payload + rhs.payload); static if (valueConvertible!(T, R) && valueConvertible!(T, R) || is(Hook == Hook1)) { // Delegate to lhs return mixin("this"~op~"rhs.payload"); } else static if (hasMember!(Hook, "hookOpBinary")) { return hook.hookOpBinary!op(payload, rhs); } else static if (hasMember!(Hook1, "hookOpBinary")) { // Delegate to rhs return mixin("this.payload"~op~"rhs"); } else static if (hasMember!(Hook, "onOverflow") && !hasMember!(Hook1, "onOverflow")) { // Delegate to lhs return mixin("this"~op~"rhs.payload"); } else static if (hasMember!(Hook1, "onOverflow") && !hasMember!(Hook, "onOverflow")) { // Delegate to rhs return mixin("this.payload"~op~"rhs"); } else { static assert(0, "Conflict between lhs and rhs hooks," " use .representation on one side to disambiguate."); } } // opBinaryRight /** */ auto opBinaryRight(string op, Lhs)(const Lhs lhs) if (isIntegral!Lhs || isFloatingPoint!Lhs || is(Lhs == bool)) { static if (hasMember!(Hook, "hookOpBinaryRight")) { auto r = hook.hookOpBinaryRight!op(lhs, payload); return Checked!(typeof(r), Hook)(r); } else static if (hasMember!(Hook, "hookOpBinary")) { auto r = hook.hookOpBinary!op(lhs, payload); return Checked!(typeof(r), Hook)(r); } else static if (is(Lhs == bool)) { return mixin("ubyte(lhs)"~op~"this"); } else static if (isFloatingPoint!Lhs) { return mixin("lhs"~op~"payload"); } else static if (hasMember!(Hook, "onOverflow")) { bool overflow; auto r = opChecked!op(lhs, T(payload), overflow); if (overflow) r = hook.onOverflow!op(42); return Checked!(typeof(r), Hook)(r); } else { // Default is built-in behavior auto r = mixin("lhs"~op~"T(payload)"); return Checked!(typeof(r), Hook)(r); } } // opOpAssign /** */ ref Checked opOpAssign(string op, Rhs)(const Rhs rhs) if (isIntegral!Rhs || isFloatingPoint!Rhs || is(Rhs == bool)) { static assert(is(typeof(mixin("payload"~op~"=rhs")) == T)); static if (hasMember!(Hook, "hookOpOpAssign")) { hook.hookOpOpAssign!op(payload, rhs); } else { alias R = typeof(payload + rhs); auto r = mixin("this"~op~"rhs").payload; static if (valueConvertible!(R, T) || !hasMember!(Hook, "onBadOpOpAssign") || op.among(">>", ">>>")) { // No need to check these payload = cast(T) r; } else { static if (isUnsigned!T && !isUnsigned!R) // Example: ushort += int const bad = r < 0 || r > max.payload; else // Some narrowing is afoot static if (R.min < min.payload) // Example: int += long const bad = r > max.payload || r < min.payload; else // Example: uint += ulong const bad = r > max.payload; if (bad) payload = hook.onBadOpOpAssign!op(payload, Rhs(rhs)); else payload = cast(T) r; } } return this; } } // representation unittest { assert(Checked!(ubyte, void)(ubyte(22)).representation == 22); } /** Force all overflows to fail with `assert(0)`. */ struct Croak { static: Dst onBadCast(Dst, Src)(Src src) { assert(0, "Bad cast"); } Lhs onBadOpOpAssign(string x, Lhs, Rhs)(Lhs, Rhs) { assert(0, "Bad opAssign"); } bool onBadOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { assert(0, "Bad comparison for equality"); } bool onBadOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { assert(0, "Bad comparison for ordering"); } typeof(~Lhs()) onOverflow(string op, Lhs)(Lhs lhs) { assert(0); } typeof(Lhs() + Rhs()) onOverflow(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { assert(0); } } unittest { Checked!(int, Croak) x; x = 42; short x1 = cast(short) x; //x += long(int.max); } // ProperCompare /** */ struct ProperCompare { static bool hookOpEquals(L, R)(L lhs, R rhs) { alias C = typeof(lhs + rhs); static if (isFloatingPoint!C) { static if (!isFloatingPoint!L) { return hookOpEquals(rhs, lhs); } else static if (!isFloatingPoint!R) { static assert(isFloatingPoint!L && !isFloatingPoint!R); auto rhs1 = C(rhs); return lhs == rhs1 && cast(R) rhs1 == rhs; } else return lhs == rhs; } else static if (valueConvertible!(L, C) && valueConvertible!(R, C)) { // Values are converted to R before comparison, cool. return lhs == rhs; } else { static assert(isUnsigned!C); static assert(isUnsigned!L != isUnsigned!R); if (lhs != rhs) return false; // R(lhs) and R(rhs) have the same bit pattern, yet may be // different due to signedness change. static if (!isUnsigned!R) { if (rhs >= 0) return true; } else { if (lhs >= 0) return true; } return false; } } static auto hookOpCmp(L, R)(L lhs, R rhs) { alias C = typeof(lhs + rhs); static if (isFloatingPoint!C) { return lhs < rhs ? C(-1) : lhs > rhs ? C(1) : lhs == rhs ? C(0) : C.init; } else { static if (!valueConvertible!(L, C) || !valueConvertible!(R, C)) { static assert(isUnsigned!C); static assert(isUnsigned!L != isUnsigned!R); if (!isUnsigned!L && lhs < 0) return -1; if (!isUnsigned!R && rhs < 0) return 1; } return lhs < rhs ? -1 : lhs > rhs; } } } unittest { alias opEqualsProper = ProperCompare.hookOpEquals; assert(opEqualsProper(42, 42)); assert(opEqualsProper(42u, 42)); assert(opEqualsProper(42, 42u)); assert(!opEqualsProper(-1, uint(-1))); assert(!opEqualsProper(uint(-1), -1)); assert(!opEqualsProper(uint(-1), -1.0)); } unittest { alias opCmpProper = ProperCompare.hookOpCmp; assert(opCmpProper(42, 42) == 0); assert(opCmpProper(42u, 42) == 0); assert(opCmpProper(42, 42u) == 0); assert(opCmpProper(-1, uint(-1)) < 0); assert(opCmpProper(uint(-1), -1) > 0); assert(opCmpProper(-1.0, -1) == 0); } unittest { auto x1 = Checked!(uint, ProperCompare)(42u); assert(x1.payload < -1); assert(x1 > -1); } // WithNaN /** */ struct WithNaN { static: enum defaultValue(T) = T.min == 0 ? T.max : T.min; enum max(T) = cast(T) (T.min == 0 ? T.max - 1 : T.max); enum min(T) = cast(T) (T.min == 0 ? T(0) : T.min + 1); Lhs hookOpCast(Lhs, Rhs)(Rhs rhs) { static if (is(Lhs == bool)) { return rhs != defaultValue!Rhs && rhs != 0; } else static if (valueConvertible!(Rhs, Lhs)) { return rhs != defaultValue!Rhs ? Lhs(rhs) : defaultValue!Lhs; } else { if (isUnsigned!Rhs || !isUnsigned!Lhs || Rhs.sizeof > Lhs.sizeof || rhs >= 0) { auto result = cast(Lhs) rhs; // If signedness is different, we need additional checks if (result == rhs && (!isUnsigned!Rhs || isUnsigned!Lhs || result >= 0)) return result; } return defaultValue!Lhs; } } Lhs onBadOpOpAssign(string x, Lhs, Rhs)(Lhs, Rhs) { return defaultValue!Lhs; } bool hookOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { return lhs != defaultValue!Lhs && lhs == rhs; } double hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { if (lhs == defaultValue!Lhs) return double.init; return lhs < rhs ? -1.0 : lhs > rhs ? 1.0 : lhs == rhs ? 0.0 : double.init; } auto hookOpUnary(string x, T)(ref T v) { static if (x == "-" || x == "~") { return v != defaultValue!T ? mixin(x~"v") : v; } else static if (x == "++") { static if (defaultValue!T == T.min) { if (v != defaultValue!T) { if (v == T.max) v = defaultValue!T; else ++v; } } else { static assert(defaultValue!T == T.max); if (v != defaultValue!T) ++v; } } else static if (x == "-") { if (v != defaultValue!T) --v; } } auto hookOpBinary(string x, L, R)(L lhs, R rhs) { alias Result = typeof(lhs + rhs); return lhs != defaultValue!L ? mixin("lhs"~x~"rhs") : defaultValue!Result; } auto hookOpBinaryRight(string x, L, R)(L lhs, R rhs) { alias Result = typeof(lhs + rhs); return rhs != defaultValue!R ? mixin("lhs"~op~"rhs") : defaultValue!Result; } void hookOpOpAssign(string x, L, R)(ref L lhs, R rhs) { if (lhs != defaultValue!L) mixin("lhs"~x~"=rhs;"); } } /// unittest { auto x1 = Checked!(int, WithNaN)(); assert(x1.payload == int.min); assert(x1 != x1); assert(!(x1 < x1)); assert(!(x1 > x1)); assert(!(x1 == x1)); ++x1; assert(x1.payload == int.min); --x1; assert(x1.payload == int.min); x1 = 42; assert(x1 == x1); assert(x1 <= x1); assert(x1 >= x1); static assert(x1.min == int.min + 1); x1 += long(int.max); } unittest { alias Smart(T) = Checked!(Checked!(T, ProperCompare), WithNaN); Smart!int x1; assert(x1 != x1); x1 = -1; assert(x1 < 1u); auto x2 = Smart!int(42); } /* Yields `true` if `T1` is "value convertible" (using terminology from C) to `T2`, where the two are integral types. That is, all of values in `T1` are also in `T2`. For example `int` is value convertible to `long` but not to `uint` or `ulong`. */ /* private enum valueConvertible(T1, T2) = isIntegral!T1 && isIntegral!T2 && is(T1 : T2) && ( isUnsigned!T1 == isUnsigned!T2 || // same signedness !isUnsigned!T2 && T2.sizeof > T1.sizeof // safely convertible ); */ template valueConvertible(T1, T2) { static if (!isIntegral!T1 || !isIntegral!T2) { enum bool valueConvertible = false; } else { enum bool valueConvertible = is(T1 : T2) && ( isUnsigned!T1 == isUnsigned!T2 || // same signedness !isUnsigned!T2 && T2.sizeof > T1.sizeof // safely convertible ); } } /** Defines binary operations with overflow checking for any two integral types. The result type obeys the language rules (even when they may be counterintuitive), and `overflow` is set if an overflow occurs (including inadvertent change of signedness, e.g. `-1` is converted to `uint`). Conceptually the behavior is: $(OL $(LI Perform the operation in infinite precision) $(LI If the infinite-precision result fits in the result type, return it and do not touch `overflow`) $(LI Otherwise, set `overflow` to `true` and return an unspecified value) ) The implementation exploits properties of types and operations to minimize additional work. */ typeof(L() + R()) opChecked(string x, L, R)(const L lhs, const R rhs, ref bool overflow) if (isIntegral!L && isIntegral!R) { alias Result = typeof(lhs + rhs); import core.checkedint; import std.algorithm : among; static if (x.among("<<", ">>", ">>>")) { // Handle shift separately from all others. The test below covers // negative rhs as well. if (unsigned(rhs) > 8 * Result.sizeof) goto fail; return mixin("lhs"~x~"rhs"); } else static if (x.among("&", "|", "^")) { // Nothing to check return mixin("lhs"~x~"rhs"); } else static if (x == "^^") { // Exponentiation is weird, handle separately return pow(lhs, rhs, overflow); } else static if (valueConvertible!(L, Result) && valueConvertible!(R, Result)) { static if (L.sizeof < Result.sizeof && R.sizeof < Result.sizeof && x.among("+", "-", "*")) { // No checks - both are value converted and result is in range return mixin("lhs"~x~"rhs"); } else static if (x == "+") { static if (isUnsigned!Result) alias impl = addu; else alias impl = adds; return impl(Result(lhs), Result(rhs), overflow); } else static if (x == "-") { static if (isUnsigned!Result) alias impl = subu; else alias impl = subs; return impl(Result(lhs), Result(rhs), overflow); } else static if (x == "*") { static if (!isUnsigned!L && !isUnsigned!R && is(L == Result)) { if (lhs == Result.min && rhs == -1) goto fail; } static if (isUnsigned!Result) alias impl = mulu; else alias impl = muls; return impl(Result(lhs), Result(rhs), overflow); } else static if (x == "/" || x == "%") { static if (!isUnsigned!L && !isUnsigned!R && is(L == Result) && op == "/") { if (lhs == Result.min && rhs == -1) goto fail; } if (rhs == 0) goto fail; return mixin("lhs"~x~"rhs"); } else static assert(0, x); } else // Mixed signs { static assert(isUnsigned!Result); static assert(isUnsigned!L != isUnsigned!R); static if (x == "+") { static if (!isUnsigned!L) { if (lhs < 0) return subu(Result(rhs), Result(-lhs), overflow); } else static if (!isUnsigned!R) { if (rhs < 0) return subu(Result(lhs), Result(-rhs), overflow); } return addu(Result(lhs), Result(rhs), overflow); } else static if (x == "-") { static if (!isUnsigned!L) { if (lhs < 0) goto fail; } else static if (!isUnsigned!R) { if (rhs < 0) return addu(Result(lhs), Result(-rhs), overflow); } return subu(Result(lhs), Result(rhs), overflow); } else static if (x == "*") { static if (!isUnsigned!L) { if (lhs < 0) goto fail; } else static if (!isUnsigned!R) { if (rhs < 0) goto fail; } return mulu(Result(lhs), Result(rhs), overflow); } else static if (x == "/" || x == "%") { static if (!isUnsigned!L) { if (lhs < 0 || rhs == 0) goto fail; } else static if (!isUnsigned!R) { if (rhs <= 0) goto fail; } return mixin("Result(lhs)"~x~"Result(rhs)"); } else static assert(0, x); } debug assert(false); fail: overflow = true; return 0; } /// unittest { bool overflow; assert(opChecked!"+"(short(1), short(1), overflow) == 2 && !overflow); assert(opChecked!"+"(1, 1, overflow) == 2 && !overflow); assert(opChecked!"+"(1, 1u, overflow) == 2 && !overflow); assert(opChecked!"+"(-1, 1u, overflow) == 0 && !overflow); assert(opChecked!"+"(1u, -1, overflow) == 0 && !overflow); } /// unittest { bool overflow; assert(opChecked!"-"(1, 1, overflow) == 0 && !overflow); assert(opChecked!"-"(1, 1u, overflow) == 0 && !overflow); assert(opChecked!"-"(1u, -1, overflow) == 2 && !overflow); assert(opChecked!"-"(-1, 1u, overflow) == 0 && overflow); } unittest { bool overflow; assert(opChecked!"*"(2, 3, overflow) == 6 && !overflow); assert(opChecked!"*"(2, 3u, overflow) == 6 && !overflow); assert(opChecked!"*"(1u, -1, overflow) == 0 && overflow); //assert(mul(-1, 1u, overflow) == uint.max - 1 && overflow); } unittest { bool overflow; assert(opChecked!"/"(6, 3, overflow) == 2 && !overflow); assert(opChecked!"/"(6, 3, overflow) == 2 && !overflow); assert(opChecked!"/"(6u, 3, overflow) == 2 && !overflow); assert(opChecked!"/"(6, 3u, overflow) == 2 && !overflow); assert(opChecked!"/"(11, 0, overflow) == 0 && overflow); overflow = false; assert(opChecked!"/"(6u, 0, overflow) == 0 && overflow); overflow = false; assert(opChecked!"/"(-6, 2u, overflow) == 0 && overflow); overflow = false; assert(opChecked!"/"(-6, 0u, overflow) == 0 && overflow); } /** */ private pure @safe nothrow @nogc auto pow(L, R)(const L lhs, const R rhs, ref bool overflow) if (isIntegral!L && isIntegral!R) { if (rhs <= 1) { if (rhs == 0) return 1; static if (!isUnsigned!R) return rhs == 1 ? lhs : (rhs == -1 && (lhs == 1 || lhs == -1)) ? lhs : 0; else return lhs; } typeof(lhs ^^ rhs) b = void; static if (!isUnsigned!L && isUnsigned!(typeof(b))) { // Need to worry about mixed-sign stuff if (lhs < 0) { if (rhs & 1) { if (lhs < 0) overflow = true; return 0; } b = -lhs; } else { b = lhs; } } else { b = lhs; } if (b == 1) return 1; if (b == -1) return (rhs & 1) ? -1 : 1; if (rhs > 63) { overflow = true; return 0; } assert((b > 1 || b < -1) && rhs > 1); return powImpl(b, cast(uint) rhs, overflow); } // Inspiration: http://www.stepanovpapers.com/PAM.pdf pure @safe nothrow @nogc private T powImpl(T)(T b, uint e, ref bool overflow) if (isIntegral!T && T.sizeof >= 4) { assert(e > 1); import core.checkedint : muls, mulu; static if (isUnsigned!T) alias mul = mulu; else alias mul = muls; T r = b; --e; // Loop invariant: r * (b ^^ e) is the actual result for (;; e /= 2) { if (e % 2) { r = mul(r, b, overflow); if (e == 1) break; } b = mul(b, b, overflow); } return r; } unittest { static void testPow(T)(T x, uint e) { bool overflow; assert(opChecked!"^^"(T(0), 0, overflow) == 1); assert(opChecked!"^^"(-2, T(0), overflow) == 1); assert(opChecked!"^^"(-2, T(1), overflow) == -2); assert(opChecked!"^^"(-1, -1, overflow) == -1); assert(opChecked!"^^"(-2, 1, overflow) == -2); assert(opChecked!"^^"(-2, -1, overflow) == 0); assert(opChecked!"^^"(-2, 4u, overflow) == 16); assert(!overflow); assert(opChecked!"^^"(-2, 3u, overflow) == 0); assert(overflow); overflow = false; assert(opChecked!"^^"(3, 64u, overflow) == 0); assert(overflow); overflow = false; foreach (uint i; 0 .. e) { assert(opChecked!"^^"(x, i, overflow) == x ^^ i); assert(!overflow); } assert(opChecked!"^^"(x, e, overflow) == x ^^ e); assert(overflow); } testPow!int(3, 21); testPow!uint(3, 21); testPow!long(3, 40); testPow!ulong(3, 41); } version(unittest) private struct CountOverflows { uint calls; auto onOverflow(string op, Lhs)(Lhs lhs) { ++calls; return mixin(op~"lhs"); } auto onOverflow(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; return mixin("lhs"~op~"rhs"); } Lhs onBadOpOpAssign(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; return mixin("lhs"~op~"=rhs"); } } version(unittest) private struct CountOpBinary { uint calls; auto hookOpBinary(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; return mixin("lhs"~op~"rhs"); } } // opBinary @nogc nothrow pure @safe unittest { auto x = Checked!(int, void)(42), y = Checked!(int, void)(142); assert(x + y == 184); assert(x + 100 == 142); assert(y - x == 100); assert(200 - x == 158); assert(y * x == 142 * 42); assert(x / 1 == 42); assert(x % 20 == 2); auto x1 = Checked!(int, CountOverflows)(42); assert(x1 + 0 == 42); assert(x1 + false == 42); assert(is(typeof(x1 + 0.5) == double)); assert(x1 + 0.5 == 42.5); assert(x1.hook.calls == 0); assert(x1 + int.max == int.max + 42); assert(x1.hook.calls == 1); assert(x1 * 2 == 84); assert(x1.hook.calls == 1); assert(x1 / 2 == 21); assert(x1.hook.calls == 1); assert(x1 % 20 == 2); assert(x1.hook.calls == 1); assert(x1 << 2 == 42 << 2); assert(x1.hook.calls == 1); assert(x1 << 42 == x1.payload << x1.payload); assert(x1.hook.calls == 2); auto x2 = Checked!(int, CountOpBinary)(42); assert(x2 + 1 == 43); assert(x2.hook.calls == 1); auto x3 = Checked!(uint, CountOverflows)(42u); assert(x3 + 1 == 43); assert(x3.hook.calls == 0); assert(x3 - 1 == 41); assert(x3.hook.calls == 0); assert(x3 + (-42) == 0); assert(x3.hook.calls == 0); assert(x3 - (-42) == 84); assert(x3.hook.calls == 0); assert(x3 * 2 == 84); assert(x3.hook.calls == 0); assert(x3 * -2 == -84); assert(x3.hook.calls == 1); assert(x3 / 2 == 21); assert(x3.hook.calls == 1); assert(x3 / -2 == 0); assert(x3.hook.calls == 2); assert(x3 ^^ 2 == 42 * 42); assert(x3.hook.calls == 2); auto x4 = Checked!(int, CountOverflows)(42); assert(x4 + 1 == 43); assert(x4.hook.calls == 0); assert(x4 + 1u == 43); assert(x4.hook.calls == 0); assert(x4 - 1 == 41); assert(x4.hook.calls == 0); assert(x4 * 2 == 84); assert(x4.hook.calls == 0); x4 = -2; assert(x4 + 2u == 0); assert(x4.hook.calls == 0); assert(x4 * 2u == -4); assert(x4.hook.calls == 1); auto x5 = Checked!(int, CountOverflows)(3); assert(x5 ^^ 0 == 1); assert(x5 ^^ 1 == 3); assert(x5 ^^ 2 == 9); assert(x5 ^^ 3 == 27); assert(x5 ^^ 4 == 81); assert(x5 ^^ 5 == 81 * 3); assert(x5 ^^ 6 == 81 * 9); } // opBinaryRight @nogc nothrow pure @safe unittest { auto x1 = Checked!(int, CountOverflows)(42); assert(1 + x1 == 43); assert(true + x1 == 43); assert(0.5 + x1 == 42.5); auto x2 = Checked!(int, void)(42); assert(x1 + x2 == 84); assert(x2 + x1 == 84); } // opOpAssign unittest { auto x1 = Checked!(int, CountOverflows)(3); assert((x1 += 2) == 5); x1 *= 2_000_000_000L; assert(x1.hook.calls == 1); auto x2 = Checked!(ushort, CountOverflows)(ushort(3)); assert((x2 += 2) == 5); assert(x2.hook.calls == 0); assert((x2 += ushort.max) == cast(ushort) (ushort(5) + ushort.max)); assert(x2.hook.calls == 1); auto x3 = Checked!(uint, CountOverflows)(3u); x3 *= ulong(2_000_000_000); assert(x3.hook.calls == 1); } // opAssign unittest { Checked!(int, void) x; x = 42; assert(x.payload == 42); x = x; assert(x.payload == 42); x = short(43); assert(x.payload == 43); x = ushort(44); assert(x.payload == 44); } unittest { static assert(!is(typeof(Checked!(short, void)(ushort(42))))); static assert(!is(typeof(Checked!(int, void)(long(42))))); static assert(!is(typeof(Checked!(int, void)(ulong(42))))); assert(Checked!(short, void)(short(42)).payload == 42); assert(Checked!(int, void)(ushort(42)).payload == 42); } // opCast @nogc nothrow pure @safe unittest { static assert(is(typeof(cast(float) Checked!(int, void)(42)) == float)); assert(cast(float) Checked!(int, void)(42) == 42); assert(is(typeof(cast(long) Checked!(int, void)(42)) == long)); assert(cast(long) Checked!(int, void)(42) == 42); static assert(is(typeof(cast(long) Checked!(uint, void)(42u)) == long)); assert(cast(long) Checked!(uint, void)(42u) == 42); auto x = Checked!(int, void)(42); if (x) {} else assert(0); x = 0; if (x) assert(0); static struct Hook1 { uint calls; Dst hookOpCast(Dst, Src)(Src value) { ++calls; return 42; } } auto y = Checked!(long, Hook1)(long.max); assert(cast(int) y == 42); assert(cast(uint) y == 42); assert(y.hook.calls == 2); static struct Hook2 { uint calls; Dst onBadCast(Dst, Src)(Src value) { ++calls; return 42; } } auto x1 = Checked!(uint, Hook2)(100u); assert(cast(ushort) x1 == 100); assert(cast(short) x1 == 100); assert(cast(float) x1 == 100); assert(cast(double) x1 == 100); assert(cast(real) x1 == 100); assert(x1.hook.calls == 0); assert(cast(int) x1 == 100); assert(x1.hook.calls == 0); x1 = uint.max; assert(cast(int) x1 == 42); assert(x1.hook.calls == 1); auto x2 = Checked!(int, Hook2)(-100); assert(cast(short) x2 == -100); assert(cast(ushort) x2 == 42); assert(cast(uint) x2 == 42); assert(cast(ulong) x2 == 42); assert(x2.hook.calls == 3); } // opEquals @nogc nothrow pure @safe unittest { assert(Checked!(int, void)(42) == 42L); assert(42UL == Checked!(int, void)(42)); static struct Hook1 { uint calls; bool hookOpEquals(Lhs, Rhs)(const Lhs lhs, const Rhs rhs) { ++calls; return lhs != rhs; } } auto x1 = Checked!(int, Hook1)(100); assert(x1 != Checked!(long, Hook1)(100)); assert(x1.hook.calls == 1); assert(x1 != 100u); assert(x1.hook.calls == 2); static struct Hook2 { uint calls; bool hookOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; return false; } } auto x2 = Checked!(int, Hook2)(-100); assert(x2 != -100); assert(x2.hook.calls == 1); assert(x2 != cast(uint) -100); assert(x2.hook.calls == 2); x2 = 100; assert(x2 != cast(uint) 100); assert(x2.hook.calls == 3); x2 = -100; auto x3 = Checked!(uint, Hook2)(100u); assert(x3 != 100); x3 = uint.max; assert(x3 != -1); assert(x2 != x3); } // opCmp @nogc nothrow pure @safe unittest { Checked!(int, void) x; assert(x <= x); assert(x < 45); assert(x < 45u); assert(x > -45); assert(x < 44.2); assert(x > -44.2); assert(!(x < double.init)); assert(!(x > double.init)); assert(!(x <= double.init)); assert(!(x >= double.init)); static struct Hook1 { uint calls; int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; return 0; } } auto x1 = Checked!(int, Hook1)(42); assert(!(x1 < 43u)); assert(!(43u < x1)); assert(x1.hook.calls == 2); static struct Hook2 { uint calls; int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; return ProperCompare.hookOpCmp(lhs, rhs); } } auto x2 = Checked!(int, Hook2)(-42); assert(x2 < 43u); assert(43u > x2); assert(x2.hook.calls == 2); x2 = 42; assert(x2 > 41u); auto x3 = Checked!(uint, Hook2)(42u); assert(x3 > 41); assert(x3 > -41); } // opUnary @nogc nothrow pure @safe unittest { auto x = Checked!(int, void)(42); assert(x == +x); static assert(is(typeof(-x) == typeof(x))); assert(-x == Checked!(int, void)(-42)); static assert(is(typeof(~x) == typeof(x))); assert(~x == Checked!(int, void)(~42)); assert(++x == 43); assert(--x == 42); static struct Hook1 { uint calls; auto hookOpUnary(string op, T)(T value) if (op == "-") { ++calls; return T(42); } auto hookOpUnary(string op, T)(T value) if (op == "~") { ++calls; return T(43); } } auto x1 = Checked!(int, Hook1)(100); assert(is(typeof(-x1) == typeof(x1))); assert(-x1 == Checked!(int, Hook1)(42)); assert(is(typeof(~x1) == typeof(x1))); assert(~x1 == Checked!(int, Hook1)(43)); assert(x1.hook.calls == 2); static struct Hook2 { uint calls; auto hookOpUnary(string op, T)(ref T value) if (op == "++") { ++calls; --value; } auto hookOpUnary(string op, T)(ref T value) if (op == "--") { ++calls; ++value; } } auto x2 = Checked!(int, Hook2)(100); assert(++x2 == 99); assert(x2 == 99); assert(--x2 == 100); assert(x2 == 100); auto x3 = Checked!(int, CountOverflows)(int.max - 1); assert(++x3 == int.max); assert(x3.hook.calls == 0); assert(++x3 == int.min); assert(x3.hook.calls == 1); assert(-x3 == int.min); assert(x3.hook.calls == 2); x3 = int.min + 1; assert(--x3 == int.min); assert(x3.hook.calls == 2); assert(--x3 == int.max); assert(x3.hook.calls == 3); } // @nogc nothrow pure @safe unittest { Checked!(int, void) x; assert(x == x); assert(x == +x); assert(x == -x); ++x; assert(x == 1); x++; assert(x == 2); x = 42; assert(x == 42); short _short = 43; x = _short; assert(x == _short); ushort _ushort = 44; x = _ushort; assert(x == _ushort); assert(x == 44.0); assert(x != 44.1); assert(x < 45); assert(x < 44.2); assert(x > -45); assert(x > -44.2); assert(cast(long) x == 44); assert(cast(short) x == 44); Checked!(uint, void) y; assert(y <= y); assert(y == 0); assert(y < x); x = -1; assert(x > y); }