| /** |
| $(SCRIPT inhibitQuickIndex = 1;) |
| |
| 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 $(LREF opChecked)$(D !"+"(x, y, overflow)) adds two integrals `x` and |
| `y` and sets `overflow` to `true` if an overflow occurred. The flag `overflow` |
| (a `bool` 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 $(LREF 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 aborts |
| execution immediately whenever involved in an operation that produces the |
| arithmetically wrong result. The accompanying convenience function $(LREF |
| checked) uses type deduction to convert a value `x` of integral type `T` to |
| `Checked!T` by means of `checked(x)`. For example: |
| |
| --- |
| void main() |
| { |
| import std.experimental.checkedint, std.stdio; |
| writeln((checked(5) + 7).get); // 12 |
| writeln((checked(10) * 1000 * 1000 * 1000).get); // Overflow |
| } |
| --- |
| |
| Similarly, $(D checked(-1) > uint(0)) aborts execution (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` in |
| release mode 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`: |
| |
| $(BOOKTABLE , |
| $(TR $(TD $(LREF Abort)) $(TD |
| fails every incorrect operation with a message to $(REF |
| stderr, std, stdio) followed by a call to `assert(0)`. It is the default |
| second parameter, i.e. `Checked!short` is the same as |
| $(D Checked!(short, Abort)). |
| )) |
| $(TR $(TD $(LREF Throw)) $(TD |
| fails every incorrect operation by throwing an exception. |
| )) |
| $(TR $(TD $(LREF Warn)) $(TD |
| prints incorrect operations to $(REF stderr, std, stdio) |
| but otherwise preserves the built-in behavior. |
| )) |
| $(TR $(TD $(LREF ProperCompare)) $(TD |
| 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 for 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. |
| )) |
| $(TR $(TD $(LREF WithNaN)) $(TD |
| reserves a special "Not a Number" (NaN) value akin to the homonym value |
| reserved for floating-point values. Once a $(D Checked!(X, WithNaN)) |
| gets this special value, it preserves and propagates it until |
| reassigned. $(LREF isNaN) can be used to query whether the object |
| is not a number. |
| )) |
| $(TR $(TD $(LREF Saturate)) $(TD |
| implements saturating arithmetic, i.e. $(D Checked!(int, Saturate)) |
| "stops" at `int.max` for all operations that would cause an `int` to |
| overflow toward infinity, and at `int.min` for all operations that would |
| correspondingly overflow toward negative infinity. |
| )) |
| ) |
| |
| |
| 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 combinations of interest include: |
| |
| $(BOOKTABLE , |
| $(TR $(TD $(D Checked!(Checked!int, ProperCompare)))) |
| $(TR $(TD |
| defines an `int` with fixed |
| comparison operators that will fail with `assert(0)` upon overflow. (Recall that |
| `Abort` 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 `Abort` will |
| intercept comparison and will fail without giving `ProperCompare` a chance to |
| intervene. |
| )) |
| $(TR $(TD)) |
| $(TR $(TDNW $(D Checked!(Checked!(int, ProperCompare), WithNaN)))) |
| $(TR $(TD |
| 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. |
| )) |
| ) |
| |
| The hook's members are looked up statically in a Design by Introspection manner |
| and are all optional. The table below illustrates the members that a hook type |
| may define and their influence over the behavior of the `Checked` type using it. |
| In the table, `hook` is an alias for `Hook` if the type `Hook` does not |
| introduce any state, or an object of type `Hook` otherwise. |
| |
| $(TABLE , |
| $(TR $(TH `Hook` member) $(TH Semantics in $(D Checked!(T, Hook))) |
| ) |
| $(TR $(TD `defaultValue`) $(TD If defined, `Hook.defaultValue!T` is used as the |
| default initializer of the payload.) |
| ) |
| $(TR $(TD `min`) $(TD If defined, `Hook.min!T` is used as the minimum value of |
| the payload.) |
| ) |
| $(TR $(TD `max`) $(TD If defined, `Hook.max!T` is used as the maximum value of |
| the payload.) |
| ) |
| $(TR $(TD `hookOpCast`) $(TD If defined, `hook.hookOpCast!U(get)` is forwarded |
| to unconditionally when the payload is to be cast to type `U`.) |
| ) |
| $(TR $(TD `onBadCast`) $(TD If defined and `hookOpCast` is $(I not) defined, |
| `onBadCast!U(get)` is forwarded to when the payload is to be cast to type `U` |
| and the cast would lose information or force a change of sign.) |
| ) |
| $(TR $(TD `hookOpEquals`) $(TD If defined, $(D hook.hookOpEquals(get, rhs)) is |
| forwarded to unconditionally when the payload is compared for equality against |
| value `rhs` of integral, floating point, or Boolean type.) |
| ) |
| $(TR $(TD `hookOpCmp`) $(TD If defined, $(D hook.hookOpCmp(get, rhs)) is |
| forwarded to unconditionally when the payload is compared for ordering against |
| value `rhs` of integral, floating point, or Boolean type.) |
| ) |
| $(TR $(TD `hookOpUnary`) $(TD If defined, `hook.hookOpUnary!op(get)` (where `op` |
| is the operator symbol) is forwarded to for unary operators `-` and `~`. In |
| addition, for unary operators `++` and `--`, `hook.hookOpUnary!op(payload)` is |
| called, where `payload` is a reference to the value wrapped by `Checked` so the |
| hook can change it.) |
| ) |
| $(TR $(TD `hookOpBinary`) $(TD If defined, $(D hook.hookOpBinary!op(get, rhs)) |
| (where `op` is the operator symbol and `rhs` is the right-hand side operand) is |
| forwarded to unconditionally for binary operators `+`, `-`, `*`, `/`, `%`, |
| `^^`, `&`, `|`, `^`, `<<`, `>>`, and `>>>`.) |
| ) |
| $(TR $(TD `hookOpBinaryRight`) $(TD If defined, $(D |
| hook.hookOpBinaryRight!op(lhs, get)) (where `op` is the operator symbol and |
| `lhs` is the left-hand side operand) is forwarded to unconditionally for binary |
| operators `+`, `-`, `*`, `/`, `%`, `^^`, `&`, `|`, `^`, `<<`, `>>`, and `>>>`.) |
| ) |
| $(TR $(TD `onOverflow`) $(TD If defined, `hook.onOverflow!op(get)` is forwarded |
| to for unary operators that overflow but only if `hookOpUnary` is not defined. |
| Unary `~` does not overflow; unary `-` overflows only when the most negative |
| value of a signed type is negated, and the result of the hook call is returned. |
| When the increment or decrement operators overflow, the payload is assigned the |
| result of `hook.onOverflow!op(get)`. When a binary operator overflows, the |
| result of $(D hook.onOverflow!op(get, rhs)) is returned, but only if `Hook` does |
| not define `hookOpBinary`.) |
| ) |
| $(TR $(TD `hookOpOpAssign`) $(TD If defined, $(D hook.hookOpOpAssign!op(payload, |
| rhs)) (where `op` is the operator symbol and `rhs` is the right-hand side |
| operand) is forwarded to unconditionally for binary operators `+=`, `-=`, `*=`, `/=`, `%=`, |
| `^^=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, and `>>>=`.) |
| ) |
| $(TR $(TD `onLowerBound`) $(TD If defined, $(D hook.onLowerBound(value, bound)) |
| (where `value` is the value being assigned) is forwarded to when the result of |
| binary operators `+=`, `-=`, `*=`, `/=`, `%=`, `^^=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, |
| and `>>>=` is smaller than the smallest value representable by `T`.) |
| ) |
| $(TR $(TD `onUpperBound`) $(TD If defined, $(D hook.onUpperBound(value, bound)) |
| (where `value` is the value being assigned) is forwarded to when the result of |
| binary operators `+=`, `-=`, `*=`, `/=`, `%=`, `^^=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, |
| and `>>>=` is larger than the largest value representable by `T`.) |
| ) |
| ) |
| |
| */ |
| module std.experimental.checkedint; |
| import std.traits : isFloatingPoint, isIntegral, isNumeric, isUnsigned, Unqual; |
| |
| /// |
| @system unittest |
| { |
| int[] concatAndAdd(int[] a, int[] b, int offset) |
| { |
| // Aborts on overflow on size computation |
| auto r = new int[(checked(a.length) + b.length).get]; |
| // Aborts on overflow on element computation |
| foreach (i; 0 .. a.length) |
| r[i] = (a[i] + checked(offset)).get; |
| foreach (i; 0 .. b.length) |
| r[i + a.length] = (b[i] + checked(offset)).get; |
| return r; |
| } |
| assert(concatAndAdd([1, 2, 3], [4, 5], -1) == [0, 1, 2, 3, 4]); |
| } |
| |
| /** |
| 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 = Abort) |
| if (isIntegral!T || is(T == Checked!(U, H), U, H)) |
| { |
| import std.algorithm.comparison : among; |
| import std.experimental.allocator.common : stateSize; |
| import std.traits : hasMember; |
| |
| /** |
| The type of the integral subject to checking. |
| */ |
| alias Representation = 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 |
| |
| // get |
| /** |
| Returns a copy of the underlying value. |
| */ |
| auto get() inout { return payload; } |
| /// |
| @safe unittest |
| { |
| auto x = checked(ubyte(42)); |
| static assert(is(typeof(x.get()) == ubyte)); |
| assert(x.get == 42); |
| const y = checked(ubyte(42)); |
| static assert(is(typeof(y.get()) == const ubyte)); |
| assert(y.get == 42); |
| } |
| |
| /** |
| Defines the minimum and maximum. These values are hookable by defining |
| `Hook.min` and/or `Hook.max`. |
| */ |
| static if (hasMember!(Hook, "min")) |
| { |
| enum Checked!(T, Hook) min = Checked!(T, Hook)(Hook.min!T); |
| /// |
| @system unittest |
| { |
| assert(Checked!short.min == -32768); |
| assert(Checked!(short, WithNaN).min == -32767); |
| assert(Checked!(uint, WithNaN).max == uint.max - 1); |
| } |
| } |
| else |
| enum Checked!(T, Hook) min = Checked(T.min); |
| /// ditto |
| static if (hasMember!(Hook, "max")) |
| enum Checked!(T, Hook) max = Checked(Hook.max!T); |
| else |
| enum Checked!(T, Hook) max = Checked(T.max); |
| |
| /** |
| 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 representation 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!(T, Hook)(rhs.get)))) |
| { |
| static if (isIntegral!U) |
| payload = rhs; |
| else |
| payload = rhs.payload; |
| } |
| /// |
| @system unittest |
| { |
| auto a = checked(42L); |
| assert(a == 42); |
| auto b = Checked!long(4242); // convert 4242 to long |
| assert(b == 4242); |
| } |
| |
| /** |
| Assignment operator. Has the same constraints as the constructor. |
| */ |
| void opAssign(U)(U rhs) if (is(typeof(Checked!(T, Hook)(rhs)))) |
| { |
| static if (isIntegral!U) |
| payload = rhs; |
| else |
| payload = rhs.payload; |
| } |
| /// |
| @system unittest |
| { |
| Checked!long a; |
| a = 42L; |
| assert(a == 42); |
| a = 4242; |
| assert(a == 4242); |
| } |
| |
| // opCast |
| /** |
| Casting operator to integral, `bool`, or floating point type. If `Hook` |
| defines `hookOpCast`, the call immediately returns |
| `hook.hookOpCast!U(get)`. Otherwise, casting to `bool` yields $(D |
| get != 0) and casting to another integral that can represent all |
| values of `T` returns `get` promoted to `U`. |
| |
| If a cast to a floating-point type is requested and `Hook` defines |
| `onBadCast`, the cast is verified by ensuring $(D get == cast(T) |
| U(get)). If that is not `true`, `hook.onBadCast!U(get)` is returned. |
| |
| If a cast to an integral type is requested and `Hook` defines `onBadCast`, |
| the cast is verified by ensuring `get` and $(D cast(U) |
| get) 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(get)` is |
| returned. |
| |
| */ |
| U opCast(U, this _)() |
| 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); |
| } |
| } |
| /// |
| @system unittest |
| { |
| assert(cast(uint) checked(42) == 42); |
| assert(cast(uint) checked!WithNaN(-42) == uint.max); |
| } |
| |
| // opEquals |
| /** |
| Compares `this` against `rhs` for equality. If `Hook` defines |
| `hookOpEquals`, the function forwards to $(D |
| hook.hookOpEquals(get, rhs)). Otherwise, the result of the |
| built-in operation $(D get == rhs) is returned. |
| |
| If `U` is also an instance of `Checked`, both hooks (left- and right-hand |
| side) are introspected for the method `hookOpEquals`. If both define it, |
| priority is given to the left-hand side. |
| |
| */ |
| bool opEquals(U, this _)(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!(W, "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; |
| } |
| |
| /// |
| static if (is(T == int) && is(Hook == void)) @safe unittest |
| { |
| static struct MyHook |
| { |
| static bool thereWereErrors; |
| static bool hookOpEquals(L, R)(L lhs, R rhs) |
| { |
| if (lhs != rhs) return false; |
| static if (isUnsigned!L && !isUnsigned!R) |
| { |
| if (lhs > 0 && rhs < 0) thereWereErrors = true; |
| } |
| else static if (isUnsigned!R && !isUnsigned!L) |
| if (lhs < 0 && rhs > 0) thereWereErrors = true; |
| // Preserve built-in behavior. |
| return true; |
| } |
| } |
| auto a = checked!MyHook(-42); |
| assert(a == uint(-42)); |
| assert(MyHook.thereWereErrors); |
| MyHook.thereWereErrors = false; |
| assert(checked!MyHook(uint(-42)) == -42); |
| assert(MyHook.thereWereErrors); |
| static struct MyHook2 |
| { |
| static bool hookOpEquals(L, R)(L lhs, R rhs) |
| { |
| return lhs == rhs; |
| } |
| } |
| MyHook.thereWereErrors = false; |
| assert(checked!MyHook2(uint(-42)) == a); |
| // Hook on left hand side takes precedence, so no errors |
| assert(!MyHook.thereWereErrors); |
| } |
| |
| // opCmp |
| /** |
| |
| Compares `this` against `rhs` for ordering. If `Hook` defines `hookOpCmp`, |
| the function forwards to $(D hook.hookOpCmp(get, rhs)). Otherwise, the |
| result of the built-in comparison operation is returned. |
| |
| If `U` is also an instance of `Checked`, both hooks (left- and right-hand |
| side) are introspected for the method `hookOpCmp`. If both define it, |
| priority is given to the left-hand side. |
| |
| */ |
| auto opCmp(U, this _)(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, this _)(Checked!(U, Hook1) rhs) |
| { |
| alias R = typeof(payload + rhs.payload); |
| static if (valueConvertible!(T, R) && valueConvertible!(U, 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(get, rhs.get); |
| } |
| else static if (hasMember!(Hook1, "hookOpCmp")) |
| { |
| return -rhs.hook.hookOpCmp(rhs.payload, get); |
| } |
| else |
| { |
| return payload < rhs.payload ? -1 : payload > rhs.payload; |
| } |
| } |
| |
| /// |
| static if (is(T == int) && is(Hook == void)) @safe unittest |
| { |
| static struct MyHook |
| { |
| static bool thereWereErrors; |
| static int hookOpCmp(L, R)(L lhs, R rhs) |
| { |
| static if (isUnsigned!L && !isUnsigned!R) |
| { |
| if (rhs < 0 && rhs >= lhs) |
| thereWereErrors = true; |
| } |
| else static if (isUnsigned!R && !isUnsigned!L) |
| { |
| if (lhs < 0 && lhs >= rhs) |
| thereWereErrors = true; |
| } |
| // Preserve built-in behavior. |
| return lhs < rhs ? -1 : lhs > rhs; |
| } |
| } |
| auto a = checked!MyHook(-42); |
| assert(a > uint(42)); |
| assert(MyHook.thereWereErrors); |
| static struct MyHook2 |
| { |
| static int hookOpCmp(L, R)(L lhs, R rhs) |
| { |
| // Default behavior |
| return lhs < rhs ? -1 : lhs > rhs; |
| } |
| } |
| MyHook.thereWereErrors = false; |
| assert(Checked!(uint, MyHook2)(uint(-42)) <= a); |
| //assert(Checked!(uint, MyHook2)(uint(-42)) >= a); |
| // Hook on left hand side takes precedence, so no errors |
| assert(!MyHook.thereWereErrors); |
| assert(a <= Checked!(uint, MyHook2)(uint(-42))); |
| assert(MyHook.thereWereErrors); |
| } |
| |
| // For coverage |
| static if (is(T == int) && is(Hook == void)) @system unittest |
| { |
| assert(checked(42) <= checked!void(42)); |
| assert(checked!void(42) <= checked(42u)); |
| assert(checked!void(42) <= checked!(void*)(42u)); |
| } |
| |
| // opUnary |
| /** |
| |
| Defines unary operators `+`, `-`, `~`, `++`, and `--`. Unary `+` is not |
| overridable and always has built-in behavior (returns `this`). For the |
| others, if `Hook` defines `hookOpUnary`, `opUnary` forwards to $(D |
| Checked!(typeof(hook.hookOpUnary!op(get)), |
| Hook)(hook.hookOpUnary!op(get))). |
| |
| If `Hook` does not define `hookOpUnary` but defines `onOverflow`, `opUnary` |
| forwards to `hook.onOverflow!op(get)` in case an overflow occurs. |
| For `++` and `--`, the payload is assigned from the result of the call to |
| `onOverflow`. |
| |
| Note that unary `-` is considered to overflow if `T` is a signed integral of |
| 32 or 64 bits and is equal to the most negative value. This is because that |
| value has no positive negation. |
| |
| */ |
| auto opUnary(string op, this _)() |
| 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 (op == "-" && isIntegral!T && T.sizeof >= 4 && |
| !isUnsigned!T && hasMember!(Hook, "onOverflow")) |
| { |
| static assert(is(typeof(-payload) == typeof(payload))); |
| bool overflow; |
| import core.checkedint : negs; |
| auto r = negs(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; |
| } |
| |
| /// |
| static if (is(T == int) && is(Hook == void)) @safe unittest |
| { |
| static struct MyHook |
| { |
| static bool thereWereErrors; |
| static L hookOpUnary(string x, L)(L lhs) |
| { |
| if (x == "-" && lhs == -lhs) thereWereErrors = true; |
| return -lhs; |
| } |
| } |
| auto a = checked!MyHook(long.min); |
| assert(a == -a); |
| assert(MyHook.thereWereErrors); |
| auto b = checked!void(42); |
| assert(++b == 43); |
| } |
| |
| // opBinary |
| /** |
| |
| Defines binary operators `+`, `-`, `*`, `/`, `%`, `^^`, `&`, `|`, `^`, `<<`, `>>`, |
| and `>>>`. If `Hook` defines `hookOpBinary`, `opBinary` forwards to $(D |
| Checked!(typeof(hook.hookOpBinary!op(get, rhs)), |
| Hook)(hook.hookOpBinary!op(get, rhs))). |
| |
| If `Hook` does not define `hookOpBinary` but defines `onOverflow`, |
| `opBinary` forwards to `hook.onOverflow!op(get, rhs)` in case an |
| overflow occurs. |
| |
| If two `Checked` instances are involved in a binary operation and both |
| define `hookOpBinary`, the left-hand side hook has priority. If both define |
| `onOverflow`, a compile-time error occurs. |
| |
| */ |
| auto opBinary(string op, Rhs)(const Rhs rhs) |
| if (isIntegral!Rhs || isFloatingPoint!Rhs || is(Rhs == bool)) |
| { |
| return opBinaryImpl!(op, Rhs, typeof(this))(rhs); |
| } |
| |
| /// ditto |
| auto opBinary(string op, Rhs)(const Rhs rhs) const |
| if (isIntegral!Rhs || isFloatingPoint!Rhs || is(Rhs == bool)) |
| { |
| return opBinaryImpl!(op, Rhs, typeof(this))(rhs); |
| } |
| |
| private auto opBinaryImpl(string op, Rhs, this _)(const Rhs rhs) |
| { |
| alias R = typeof(mixin("payload" ~ op ~ "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) |
| { |
| return opBinaryImpl2!(op, U, Hook1, typeof(this))(rhs); |
| } |
| |
| /// ditto |
| auto opBinary(string op, U, Hook1)(Checked!(U, Hook1) rhs) const |
| { |
| return opBinaryImpl2!(op, U, Hook1, typeof(this))(rhs); |
| } |
| |
| private |
| auto opBinaryImpl2(string op, U, Hook1, this _)(Checked!(U, Hook1) rhs) |
| { |
| alias R = typeof(get + rhs.payload); |
| static if (valueConvertible!(T, R) && valueConvertible!(U, 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 .get on one side to disambiguate."); |
| } |
| } |
| |
| static if (is(T == int) && is(Hook == void)) @system unittest |
| { |
| const a = checked(42); |
| assert(a + 1 == 43); |
| assert(a + checked(uint(42)) == 84); |
| assert(checked(42) + checked!void(42u) == 84); |
| assert(checked!void(42) + checked(42u) == 84); |
| |
| static struct MyHook |
| { |
| static uint tally; |
| static auto hookOpBinary(string x, L, R)(L lhs, R rhs) |
| { |
| ++tally; |
| return mixin("lhs" ~ x ~ "rhs"); |
| } |
| } |
| assert(checked!MyHook(42) + checked(42u) == 84); |
| assert(checked!void(42) + checked!MyHook(42u) == 84); |
| assert(MyHook.tally == 2); |
| } |
| |
| // opBinaryRight |
| /** |
| |
| Defines binary operators `+`, `-`, `*`, `/`, `%`, `^^`, `&`, `|`, `^`, `<<`, |
| `>>`, and `>>>` for the case when a built-in numeric or Boolean type is on |
| the left-hand side, and a `Checked` instance is on the right-hand side. |
| |
| */ |
| auto opBinaryRight(string op, Lhs)(const Lhs lhs) |
| if (isIntegral!Lhs || isFloatingPoint!Lhs || is(Lhs == bool)) |
| { |
| return opBinaryRightImpl!(op, Lhs, typeof(this))(lhs); |
| } |
| |
| /// ditto |
| auto opBinaryRight(string op, Lhs)(const Lhs lhs) const |
| if (isIntegral!Lhs || isFloatingPoint!Lhs || is(Lhs == bool)) |
| { |
| return opBinaryRightImpl!(op, Lhs, typeof(this))(lhs); |
| } |
| |
| private auto opBinaryRightImpl(string op, Lhs, this _)(const Lhs lhs) |
| { |
| 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); |
| } |
| } |
| |
| static if (is(T == int) && is(Hook == void)) @system unittest |
| { |
| assert(1 + checked(1) == 2); |
| static uint tally; |
| static struct MyHook |
| { |
| static auto hookOpBinaryRight(string x, L, R)(L lhs, R rhs) |
| { |
| ++tally; |
| return mixin("lhs" ~ x ~ "rhs"); |
| } |
| } |
| assert(1 + checked!MyHook(1) == 2); |
| assert(tally == 1); |
| |
| immutable x1 = checked(1); |
| assert(1 + x1 == 2); |
| immutable x2 = checked!MyHook(1); |
| assert(1 + x2 == 2); |
| assert(tally == 2); |
| } |
| |
| // opOpAssign |
| /** |
| |
| Defines operators `+=`, `-=`, `*=`, `/=`, `%=`, `^^=`, `&=`, `|=`, `^=`, |
| `<<=`, `>>=`, and `>>>=`. |
| |
| If `Hook` defines `hookOpOpAssign`, `opOpAssign` forwards to |
| `hook.hookOpOpAssign!op(payload, rhs)`, where `payload` is a reference to |
| the internally held data so the hook can change it. |
| |
| Otherwise, the operator first evaluates $(D auto result = |
| opBinary!op(payload, rhs).payload), which is subject to the hooks in |
| `opBinary`. Then, if `result` is less than $(D Checked!(T, Hook).min) and if |
| `Hook` defines `onLowerBound`, the payload is assigned from $(D |
| hook.onLowerBound(result, min)). If `result` is greater than $(D Checked!(T, |
| Hook).max) and if `Hook` defines `onUpperBound`, the payload is assigned |
| from $(D hook.onUpperBound(result, min)). |
| |
| In all other cases, the built-in behavior is carried out. |
| |
| Params: |
| op = The operator involved (without the `"="`, e.g. `"+"` for `"+="` etc) |
| rhs = The right-hand side of the operator (left-hand side is `this`) |
| |
| Returns: A reference to `this`. |
| */ |
| ref Checked opOpAssign(string op, Rhs)(const Rhs rhs) return |
| 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(get + rhs); |
| auto r = opBinary!op(rhs).get; |
| import std.conv : unsigned; |
| |
| static if (ProperCompare.hookOpCmp(R.min, min.get) < 0 && |
| hasMember!(Hook, "onLowerBound")) |
| { |
| if (ProperCompare.hookOpCmp(r, min.get) < 0) |
| { |
| // Example: Checked!uint(1) += int(-3) |
| payload = hook.onLowerBound(r, min.get); |
| return this; |
| } |
| } |
| static if (ProperCompare.hookOpCmp(max.get, R.max) < 0 && |
| hasMember!(Hook, "onUpperBound")) |
| { |
| if (ProperCompare.hookOpCmp(r, max.get) > 0) |
| { |
| // Example: Checked!uint(1) += long(uint.max) |
| payload = hook.onUpperBound(r, max.get); |
| return this; |
| } |
| } |
| payload = cast(T) r; |
| } |
| return this; |
| } |
| |
| /// |
| static if (is(T == int) && is(Hook == void)) @safe unittest |
| { |
| static struct MyHook |
| { |
| static bool thereWereErrors; |
| static T onLowerBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| thereWereErrors = true; |
| return bound; |
| } |
| static T onUpperBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| thereWereErrors = true; |
| return bound; |
| } |
| } |
| auto x = checked!MyHook(byte.min); |
| x -= 1; |
| assert(MyHook.thereWereErrors); |
| MyHook.thereWereErrors = false; |
| x = byte.max; |
| x += 1; |
| assert(MyHook.thereWereErrors); |
| } |
| } |
| |
| /** |
| |
| Convenience function that turns an integral into the corresponding `Checked` |
| instance by using template argument deduction. The hook type may be specified |
| (by default `Abort`). |
| |
| */ |
| Checked!(T, Hook) checked(Hook = Abort, T)(const T value) |
| if (is(typeof(Checked!(T, Hook)(value)))) |
| { |
| return Checked!(T, Hook)(value); |
| } |
| |
| /// |
| @system unittest |
| { |
| static assert(is(typeof(checked(42)) == Checked!int)); |
| assert(checked(42) == Checked!int(42)); |
| static assert(is(typeof(checked!WithNaN(42)) == Checked!(int, WithNaN))); |
| assert(checked!WithNaN(42) == Checked!(int, WithNaN)(42)); |
| } |
| |
| // get |
| @safe unittest |
| { |
| void test(T)() |
| { |
| assert(Checked!(T, void)(ubyte(22)).get == 22); |
| } |
| test!ubyte; |
| test!(const ubyte); |
| test!(immutable ubyte); |
| } |
| |
| // Abort |
| /** |
| |
| Force all integral errors to fail by printing an error message to `stderr` and |
| then abort the program. `Abort` is the default second argument for `Checked`. |
| |
| */ |
| struct Abort |
| { |
| static: |
| /** |
| |
| Called automatically upon a bad cast (one that loses precision or attempts |
| to convert a negative value to an unsigned type). The source type is `Src` |
| and the destination type is `Dst`. |
| |
| Params: |
| src = The source of the cast |
| |
| Returns: Nominally the result is the desired value of the cast operation, |
| which will be forwarded as the result of the cast. For `Abort`, the |
| function never returns because it aborts the program. |
| |
| */ |
| Dst onBadCast(Dst, Src)(Src src) |
| { |
| Warn.onBadCast!Dst(src); |
| assert(0); |
| } |
| |
| /** |
| |
| Called automatically upon a bounds error. |
| |
| Params: |
| rhs = The right-hand side value in the assignment, after the operator has |
| been evaluated |
| bound = The value of the bound being violated |
| |
| Returns: Nominally the result is the desired value of the operator, which |
| will be forwarded as result. For `Abort`, the function never returns because |
| it aborts the program. |
| |
| */ |
| T onLowerBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| Warn.onLowerBound(rhs, bound); |
| assert(0); |
| } |
| /// ditto |
| T onUpperBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| Warn.onUpperBound(rhs, bound); |
| assert(0); |
| } |
| |
| /** |
| |
| Called automatically upon a comparison for equality. In case of a erroneous |
| comparison (one that would make a signed negative value appear equal to an |
| unsigned positive value), this hook issues `assert(0)` which terminates the |
| application. |
| |
| Params: |
| lhs = The first argument of `Checked`, e.g. `int` if the left-hand side of |
| the operator is `Checked!int` |
| rhs = The right-hand side type involved in the operator |
| |
| Returns: Upon a correct comparison, returns the result of the comparison. |
| Otherwise, the function terminates the application so it never returns. |
| |
| */ |
| static bool hookOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| bool error; |
| auto result = opChecked!"=="(lhs, rhs, error); |
| if (error) |
| { |
| Warn.hookOpEquals(lhs, rhs); |
| assert(0); |
| } |
| return result; |
| } |
| |
| /** |
| |
| Called automatically upon a comparison for ordering using one of the |
| operators `<`, `<=`, `>`, or `>=`. In case the comparison is erroneous (i.e. |
| it would make a signed negative value appear greater than or equal to an |
| unsigned positive value), then application is terminated with `assert(0)`. |
| Otherwise, the three-state result is returned (positive if $(D lhs > rhs), |
| negative if $(D lhs < rhs), `0` otherwise). |
| |
| Params: |
| lhs = The first argument of `Checked`, e.g. `int` if the left-hand side of |
| the operator is `Checked!int` |
| rhs = The right-hand side type involved in the operator |
| |
| Returns: For correct comparisons, returns a positive integer if $(D lhs > |
| rhs), a negative integer if $(D lhs < rhs), `0` if the two are equal. Upon |
| a mistaken comparison such as $(D int(-1) < uint(0)), the function never |
| returns because it aborts the program. |
| |
| */ |
| int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| bool error; |
| auto result = opChecked!"cmp"(lhs, rhs, error); |
| if (error) |
| { |
| Warn.hookOpCmp(lhs, rhs); |
| assert(0); |
| } |
| return result; |
| } |
| |
| /** |
| |
| Called automatically upon an overflow during a unary or binary operation. |
| |
| Params: |
| x = The operator, e.g. `-` |
| lhs = The left-hand side (or sole) argument |
| rhs = The right-hand side type involved in the operator |
| |
| Returns: Nominally the result is the desired value of the operator, which |
| will be forwarded as result. For `Abort`, the function never returns because |
| it aborts the program. |
| |
| */ |
| typeof(~Lhs()) onOverflow(string x, Lhs)(Lhs lhs) |
| { |
| Warn.onOverflow!x(lhs); |
| assert(0); |
| } |
| /// ditto |
| typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| Warn.onOverflow!x(lhs, rhs); |
| assert(0); |
| } |
| } |
| |
| @system unittest |
| { |
| void test(T)() |
| { |
| Checked!(int, Abort) x; |
| x = 42; |
| auto x1 = cast(T) x; |
| assert(x1 == 42); |
| //x1 += long(int.max); |
| } |
| test!short; |
| test!(const short); |
| test!(immutable short); |
| } |
| |
| |
| // Throw |
| /** |
| |
| Force all integral errors to fail by throwing an exception of type |
| `Throw.CheckFailure`. The message coming with the error is similar to the one |
| printed by `Warn`. |
| |
| */ |
| struct Throw |
| { |
| /** |
| Exception type thrown upon any failure. |
| */ |
| static class CheckFailure : Exception |
| { |
| this(T...)(string f, T vals) |
| { |
| import std.format : format; |
| super(format(f, vals)); |
| } |
| } |
| |
| /** |
| |
| Called automatically upon a bad cast (one that loses precision or attempts |
| to convert a negative value to an unsigned type). The source type is `Src` |
| and the destination type is `Dst`. |
| |
| Params: |
| src = The source of the cast |
| |
| Returns: Nominally the result is the desired value of the cast operation, |
| which will be forwarded as the result of the cast. For `Throw`, the |
| function never returns because it throws an exception. |
| |
| */ |
| static Dst onBadCast(Dst, Src)(Src src) |
| { |
| throw new CheckFailure("Erroneous cast: cast(%s) %s(%s)", |
| Dst.stringof, Src.stringof, src); |
| } |
| |
| /** |
| |
| Called automatically upon a bounds error. |
| |
| Params: |
| rhs = The right-hand side value in the assignment, after the operator has |
| been evaluated |
| bound = The value of the bound being violated |
| |
| Returns: Nominally the result is the desired value of the operator, which |
| will be forwarded as result. For `Throw`, the function never returns because |
| it throws. |
| |
| */ |
| static T onLowerBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| throw new CheckFailure("Lower bound error: %s(%s) < %s(%s)", |
| Rhs.stringof, rhs, T.stringof, bound); |
| } |
| /// ditto |
| static T onUpperBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| throw new CheckFailure("Upper bound error: %s(%s) > %s(%s)", |
| Rhs.stringof, rhs, T.stringof, bound); |
| } |
| |
| /** |
| |
| Called automatically upon a comparison for equality. Throws upon an |
| erroneous comparison (one that would make a signed negative value appear |
| equal to an unsigned positive value). |
| |
| Params: |
| lhs = The first argument of `Checked`, e.g. `int` if the left-hand side of |
| the operator is `Checked!int` |
| rhs = The right-hand side type involved in the operator |
| |
| Returns: The result of the comparison. |
| |
| Throws: `CheckFailure` if the comparison is mathematically erroneous. |
| |
| */ |
| static bool hookOpEquals(L, R)(L lhs, R rhs) |
| { |
| bool error; |
| auto result = opChecked!"=="(lhs, rhs, error); |
| if (error) |
| { |
| throw new CheckFailure("Erroneous comparison: %s(%s) == %s(%s)", |
| L.stringof, lhs, R.stringof, rhs); |
| } |
| return result; |
| } |
| |
| /** |
| |
| Called automatically upon a comparison for ordering using one of the |
| operators `<`, `<=`, `>`, or `>=`. In case the comparison is erroneous (i.e. |
| it would make a signed negative value appear greater than or equal to an |
| unsigned positive value), throws a `Throw.CheckFailure` exception. |
| Otherwise, the three-state result is returned (positive if $(D lhs > rhs), |
| negative if $(D lhs < rhs), `0` otherwise). |
| |
| Params: |
| lhs = The first argument of `Checked`, e.g. `int` if the left-hand side of |
| the operator is `Checked!int` |
| rhs = The right-hand side type involved in the operator |
| |
| Returns: For correct comparisons, returns a positive integer if $(D lhs > |
| rhs), a negative integer if $(D lhs < rhs), `0` if the two are equal. |
| |
| Throws: Upon a mistaken comparison such as $(D int(-1) < uint(0)), the |
| function never returns because it throws a `Throw.CheckedFailure` exception. |
| |
| */ |
| static int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| bool error; |
| auto result = opChecked!"cmp"(lhs, rhs, error); |
| if (error) |
| { |
| throw new CheckFailure("Erroneous ordering comparison: %s(%s) and %s(%s)", |
| Lhs.stringof, lhs, Rhs.stringof, rhs); |
| } |
| return result; |
| } |
| |
| /** |
| |
| Called automatically upon an overflow during a unary or binary operation. |
| |
| Params: |
| x = The operator, e.g. `-` |
| lhs = The left-hand side (or sole) argument |
| rhs = The right-hand side type involved in the operator |
| |
| Returns: Nominally the result is the desired value of the operator, which |
| will be forwarded as result. For `Throw`, the function never returns because |
| it throws an exception. |
| |
| */ |
| static typeof(~Lhs()) onOverflow(string x, Lhs)(Lhs lhs) |
| { |
| throw new CheckFailure("Overflow on unary operator: %s%s(%s)", |
| x, Lhs.stringof, lhs); |
| } |
| /// ditto |
| static typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| throw new CheckFailure("Overflow on binary operator: %s(%s) %s %s(%s)", |
| Lhs.stringof, lhs, x, Rhs.stringof, rhs); |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| void test(T)() |
| { |
| Checked!(int, Throw) x; |
| x = 42; |
| auto x1 = cast(T) x; |
| assert(x1 == 42); |
| x = T.max + 1; |
| import std.exception : assertThrown, assertNotThrown; |
| assertThrown(cast(T) x); |
| x = x.max; |
| assertThrown(x += 42); |
| assertThrown(x += 42L); |
| x = x.min; |
| assertThrown(-x); |
| assertThrown(x -= 42); |
| assertThrown(x -= 42L); |
| x = -1; |
| assertNotThrown(x == -1); |
| assertThrown(x == uint(-1)); |
| assertNotThrown(x <= -1); |
| assertThrown(x <= uint(-1)); |
| } |
| test!short; |
| test!(const short); |
| test!(immutable short); |
| } |
| |
| // Warn |
| /** |
| Hook that prints to `stderr` a trace of all integral errors, without affecting |
| default behavior. |
| */ |
| struct Warn |
| { |
| import std.stdio : stderr; |
| static: |
| /** |
| |
| Called automatically upon a bad cast from `src` to type `Dst` (one that |
| loses precision or attempts to convert a negative value to an unsigned |
| type). |
| |
| Params: |
| src = The source of the cast |
| Dst = The target type of the cast |
| |
| Returns: `cast(Dst) src` |
| |
| */ |
| Dst onBadCast(Dst, Src)(Src src) |
| { |
| stderr.writefln("Erroneous cast: cast(%s) %s(%s)", |
| Dst.stringof, Src.stringof, src); |
| return cast(Dst) src; |
| } |
| |
| /** |
| |
| Called automatically upon a bad `opOpAssign` call (one that loses precision |
| or attempts to convert a negative value to an unsigned type). |
| |
| Params: |
| rhs = The right-hand side value in the assignment, after the operator has |
| been evaluated |
| bound = The bound being violated |
| |
| Returns: `cast(Lhs) rhs` |
| */ |
| Lhs onLowerBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| stderr.writefln("Lower bound error: %s(%s) < %s(%s)", |
| Rhs.stringof, rhs, T.stringof, bound); |
| return cast(T) rhs; |
| } |
| /// ditto |
| T onUpperBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| stderr.writefln("Upper bound error: %s(%s) > %s(%s)", |
| Rhs.stringof, rhs, T.stringof, bound); |
| return cast(T) rhs; |
| } |
| |
| /** |
| |
| Called automatically upon a comparison for equality. In case of an Erroneous |
| comparison (one that would make a signed negative value appear equal to an |
| unsigned positive value), writes a warning message to `stderr` as a side |
| effect. |
| |
| Params: |
| lhs = The first argument of `Checked`, e.g. `int` if the left-hand side of |
| the operator is `Checked!int` |
| rhs = The right-hand side type involved in the operator |
| |
| Returns: In all cases the function returns the built-in result of $(D lhs == |
| rhs). |
| |
| */ |
| bool hookOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| bool error; |
| auto result = opChecked!"=="(lhs, rhs, error); |
| if (error) |
| { |
| stderr.writefln("Erroneous comparison: %s(%s) == %s(%s)", |
| Lhs.stringof, lhs, Rhs.stringof, rhs); |
| return lhs == rhs; |
| } |
| return result; |
| } |
| |
| /// |
| @system unittest |
| { |
| auto x = checked!Warn(-42); |
| // Passes |
| assert(x == -42); |
| // Passes but prints a warning |
| // assert(x == uint(-42)); |
| } |
| |
| /** |
| |
| Called automatically upon a comparison for ordering using one of the |
| operators `<`, `<=`, `>`, or `>=`. In case the comparison is erroneous (i.e. |
| it would make a signed negative value appear greater than or equal to an |
| unsigned positive value), then a warning message is printed to `stderr`. |
| |
| Params: |
| lhs = The first argument of `Checked`, e.g. `int` if the left-hand side of |
| the operator is `Checked!int` |
| rhs = The right-hand side type involved in the operator |
| |
| Returns: In all cases, returns $(D lhs < rhs ? -1 : lhs > rhs). The result |
| is not autocorrected in case of an erroneous comparison. |
| |
| */ |
| int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| bool error; |
| auto result = opChecked!"cmp"(lhs, rhs, error); |
| if (error) |
| { |
| stderr.writefln("Erroneous ordering comparison: %s(%s) and %s(%s)", |
| Lhs.stringof, lhs, Rhs.stringof, rhs); |
| return lhs < rhs ? -1 : lhs > rhs; |
| } |
| return result; |
| } |
| |
| /// |
| @system unittest |
| { |
| auto x = checked!Warn(-42); |
| // Passes |
| assert(x <= -42); |
| // Passes but prints a warning |
| // assert(x <= uint(-42)); |
| } |
| |
| /** |
| |
| Called automatically upon an overflow during a unary or binary operation. |
| |
| Params: |
| x = The operator involved |
| Lhs = The first argument of `Checked`, e.g. `int` if the left-hand side of |
| the operator is `Checked!int` |
| Rhs = The right-hand side type involved in the operator |
| |
| Returns: $(D mixin(x ~ "lhs")) for unary, $(D mixin("lhs" ~ x ~ "rhs")) for |
| binary |
| |
| */ |
| typeof(~Lhs()) onOverflow(string x, Lhs)(ref Lhs lhs) |
| { |
| stderr.writefln("Overflow on unary operator: %s%s(%s)", |
| x, Lhs.stringof, lhs); |
| return mixin(x ~ "lhs"); |
| } |
| /// ditto |
| typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| stderr.writefln("Overflow on binary operator: %s(%s) %s %s(%s)", |
| Lhs.stringof, lhs, x, Rhs.stringof, rhs); |
| return mixin("lhs" ~ x ~ "rhs"); |
| } |
| } |
| |
| /// |
| @system unittest |
| { |
| auto x = checked!Warn(42); |
| short x1 = cast(short) x; |
| //x += long(int.max); |
| auto y = checked!Warn(cast(const int) 42); |
| short y1 = cast(const byte) y; |
| } |
| |
| // ProperCompare |
| /** |
| |
| Hook that provides arithmetically correct comparisons for equality and ordering. |
| Comparing an object of type $(D Checked!(X, ProperCompare)) against another |
| integral (for equality or ordering) ensures that no surprising conversions from |
| signed to unsigned integral occur before the comparison. Using $(D Checked!(X, |
| ProperCompare)) on either side of a comparison for equality against a |
| floating-point number makes sure the integral can be properly converted to the |
| floating point type, thus making sure equality is transitive. |
| |
| */ |
| struct ProperCompare |
| { |
| /** |
| Hook for `==` and `!=` that ensures comparison against integral values has |
| the behavior expected by the usual arithmetic rules. The built-in semantics |
| yield surprising behavior when comparing signed values against unsigned |
| values for equality, for example $(D uint.max == -1) or $(D -1_294_967_296 == |
| 3_000_000_000u). The call $(D hookOpEquals(x, y)) returns `true` if and only |
| if `x` and `y` represent the same arithmetic number. |
| |
| If one of the numbers is an integral and the other is a floating-point |
| number, $(D hookOpEquals(x, y)) returns `true` if and only if the integral |
| can be converted exactly (without approximation) to the floating-point |
| number. This is in order to preserve transitivity of equality: if $(D |
| hookOpEquals(x, y)) and $(D hookOpEquals(y, z)) then $(D hookOpEquals(y, |
| z)), in case `x`, `y`, and `z` are a mix of integral and floating-point |
| numbers. |
| |
| Params: |
| lhs = The left-hand side of the comparison for equality |
| rhs = The right-hand side of the comparison for equality |
| |
| Returns: |
| The result of the comparison, `true` if the values are equal |
| */ |
| 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 |
| { |
| bool error; |
| auto result = opChecked!"=="(lhs, rhs, error); |
| if (error) |
| { |
| // Only possible error is a wrong "true" |
| return false; |
| } |
| return result; |
| } |
| } |
| |
| /** |
| Hook for `<`, `<=`, `>`, and `>=` that ensures comparison against integral |
| values has the behavior expected by the usual arithmetic rules. The built-in |
| semantics yield surprising behavior when comparing signed values against |
| unsigned values, for example $(D 0u < -1). The call $(D hookOpCmp(x, y)) |
| returns `-1` if and only if `x` is smaller than `y` in abstract arithmetic |
| sense. |
| |
| If one of the numbers is an integral and the other is a floating-point |
| number, $(D hookOpEquals(x, y)) returns a floating-point number that is `-1` |
| if `x < y`, `0` if `x == y`, `1` if `x > y`, and `NaN` if the floating-point |
| number is `NaN`. |
| |
| Params: |
| lhs = The left-hand side of the comparison for ordering |
| rhs = The right-hand side of the comparison for ordering |
| |
| Returns: |
| The result of the comparison (negative if $(D lhs < rhs), positive if $(D |
| lhs > rhs), `0` if the values are equal) |
| */ |
| 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; |
| } |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| alias opEqualsProper = ProperCompare.hookOpEquals; |
| assert(opEqualsProper(42, 42)); |
| assert(opEqualsProper(42.0, 42.0)); |
| assert(opEqualsProper(42u, 42)); |
| assert(opEqualsProper(42, 42u)); |
| assert(-1 == 4294967295u); |
| assert(!opEqualsProper(-1, 4294967295u)); |
| assert(!opEqualsProper(const uint(-1), -1)); |
| assert(!opEqualsProper(uint(-1), -1.0)); |
| assert(3_000_000_000U == -1_294_967_296); |
| assert(!opEqualsProper(3_000_000_000U, -1_294_967_296)); |
| } |
| |
| @safe unittest |
| { |
| alias opCmpProper = ProperCompare.hookOpCmp; |
| assert(opCmpProper(42, 42) == 0); |
| assert(opCmpProper(42, 42.0) == 0); |
| assert(opCmpProper(41, 42.0) < 0); |
| assert(opCmpProper(42, 41.0) > 0); |
| import std.math : isNaN; |
| assert(isNaN(opCmpProper(41, double.init))); |
| 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); |
| } |
| |
| @safe unittest |
| { |
| auto x1 = Checked!(uint, ProperCompare)(42u); |
| assert(x1.get < -1); |
| assert(x1 > -1); |
| } |
| |
| // WithNaN |
| /** |
| |
| Hook that reserves a special value as a "Not a Number" representative. For |
| signed integrals, the reserved value is `T.min`. For signed integrals, the |
| reserved value is `T.max`. |
| |
| The default value of a $(D Checked!(X, WithNaN)) is its NaN value, so care must |
| be taken that all variables are explicitly initialized. Any arithmetic and logic |
| operation involving at least on NaN becomes NaN itself. All of $(D a == b), $(D |
| a < b), $(D a > b), $(D a <= b), $(D a >= b) yield `false` if at least one of |
| `a` and `b` is NaN. |
| |
| */ |
| struct WithNaN |
| { |
| static: |
| /** |
| The default value used for values not explicitly initialized. It is the NaN |
| value, i.e. `T.min` for signed integrals and `T.max` for unsigned integrals. |
| */ |
| enum T defaultValue(T) = T.min == 0 ? T.max : T.min; |
| /** |
| The maximum value representable is $(D T.max) for signed integrals, $(D |
| T.max - 1) for unsigned integrals. The minimum value representable is $(D |
| T.min + 1) for signed integrals, $(D 0) for unsigned integrals. |
| */ |
| enum T max(T) = cast(T) (T.min == 0 ? T.max - 1 : T.max); |
| /// ditto |
| enum T min(T) = cast(T) (T.min == 0 ? T(0) : T.min + 1); |
| |
| /** |
| If `rhs` is `WithNaN.defaultValue!Rhs`, returns |
| `WithNaN.defaultValue!Lhs`. Otherwise, returns $(D cast(Lhs) rhs). |
| |
| Params: |
| rhs = the value being cast (`Rhs` is the first argument to `Checked`) |
| Lhs = the target type of the cast |
| |
| Returns: The result of the cast operation. |
| */ |
| 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 |
| { |
| // Not value convertible, only viable option is rhs fits within the |
| // bounds of Lhs |
| static if (ProperCompare.hookOpCmp(Rhs.min, Lhs.min) < 0) |
| { |
| // Example: hookOpCast!short(int(42)), hookOpCast!uint(int(42)) |
| if (ProperCompare.hookOpCmp(rhs, Lhs.min) < 0) |
| return defaultValue!Lhs; |
| } |
| static if (ProperCompare.hookOpCmp(Rhs.max, Lhs.max) > 0) |
| { |
| // Example: hookOpCast!int(uint(42)) |
| if (ProperCompare.hookOpCmp(rhs, Lhs.max) > 0) |
| return defaultValue!Lhs; |
| } |
| return cast(Lhs) rhs; |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| auto x = checked!WithNaN(422); |
| assert((cast(ubyte) x) == 255); |
| x = checked!WithNaN(-422); |
| assert((cast(byte) x) == -128); |
| assert(cast(short) x == -422); |
| assert(cast(bool) x); |
| x = x.init; // set back to NaN |
| assert(x != true); |
| assert(x != false); |
| } |
| |
| /** |
| |
| Returns `false` if $(D lhs == WithNaN.defaultValue!Lhs), $(D lhs == rhs) |
| otherwise. |
| |
| Params: |
| lhs = The left-hand side of the comparison (`Lhs` is the first argument to |
| `Checked`) |
| rhs = The right-hand side of the comparison |
| |
| Returns: `lhs != WithNaN.defaultValue!Lhs && lhs == rhs` |
| */ |
| bool hookOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| return lhs != defaultValue!Lhs && lhs == rhs; |
| } |
| |
| /** |
| |
| If $(D lhs == WithNaN.defaultValue!Lhs), returns `double.init`. Otherwise, |
| has the same semantics as the default comparison. |
| |
| Params: |
| lhs = The left-hand side of the comparison (`Lhs` is the first argument to |
| `Checked`) |
| rhs = The right-hand side of the comparison |
| |
| Returns: `double.init` if `lhs == WitnNaN.defaultValue!Lhs`, `-1.0` if $(D |
| lhs < rhs), `0.0` if $(D lhs == rhs), `1.0` if $(D 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; |
| } |
| |
| /// |
| @safe unittest |
| { |
| Checked!(int, WithNaN) x; |
| assert(!(x < 0) && !(x > 0) && !(x == 0)); |
| x = 1; |
| assert(x > 0 && !(x < 0) && !(x == 0)); |
| } |
| |
| /** |
| Defines hooks for unary operators `-`, `~`, `++`, and `--`. |
| |
| For `-` and `~`, if $(D v == WithNaN.defaultValue!T), returns |
| `WithNaN.defaultValue!T`. Otherwise, the semantics is the same as for the |
| built-in operator. |
| |
| For `++` and `--`, if $(D v == WithNaN.defaultValue!Lhs) or the operation |
| would result in an overflow, sets `v` to `WithNaN.defaultValue!T`. |
| Otherwise, the semantics is the same as for the built-in operator. |
| |
| Params: |
| x = The operator symbol |
| v = The left-hand side of the comparison (`T` is the first argument to |
| `Checked`) |
| |
| Returns: $(UL $(LI For $(D x == "-" || x == "~"): If $(D v == |
| WithNaN.defaultValue!T), the function returns `WithNaN.defaultValue!T`. |
| Otherwise it returns the normal result of the operator.) $(LI For $(D x == |
| "++" || x == "--"): The function returns `void`.)) |
| |
| */ |
| 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; |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| Checked!(int, WithNaN) x; |
| ++x; |
| assert(x.isNaN); |
| x = 1; |
| assert(!x.isNaN); |
| x = -x; |
| ++x; |
| assert(!x.isNaN); |
| } |
| |
| @safe unittest // for coverage |
| { |
| Checked!(uint, WithNaN) y; |
| ++y; |
| assert(y.isNaN); |
| } |
| |
| /** |
| Defines hooks for binary operators `+`, `-`, `*`, `/`, `%`, `^^`, `&`, `|`, |
| `^`, `<<`, `>>`, and `>>>` for cases where a `Checked` object is the |
| left-hand side operand. If $(D lhs == WithNaN.defaultValue!Lhs), returns |
| $(D WithNaN.defaultValue!(typeof(lhs + rhs))) without evaluating the |
| operand. Otherwise, evaluates the operand. If evaluation does not overflow, |
| returns the result. Otherwise, returns $(D WithNaN.defaultValue!(typeof(lhs |
| + rhs))). |
| |
| Params: |
| x = The operator symbol |
| lhs = The left-hand side operand (`Lhs` is the first argument to `Checked`) |
| rhs = The right-hand side operand |
| |
| Returns: If $(D lhs != WithNaN.defaultValue!Lhs) and the operator does not |
| overflow, the function returns the same result as the built-in operator. In |
| all other cases, returns $(D WithNaN.defaultValue!(typeof(lhs + rhs))). |
| */ |
| auto hookOpBinary(string x, L, R)(L lhs, R rhs) |
| { |
| alias Result = typeof(lhs + rhs); |
| if (lhs != defaultValue!L) |
| { |
| bool error; |
| auto result = opChecked!x(lhs, rhs, error); |
| if (!error) return result; |
| } |
| return defaultValue!Result; |
| } |
| |
| /// |
| @safe unittest |
| { |
| Checked!(int, WithNaN) x; |
| assert((x + 1).isNaN); |
| x = 100; |
| assert(!(x + 1).isNaN); |
| } |
| |
| /** |
| Defines hooks for binary operators `+`, `-`, `*`, `/`, `%`, `^^`, `&`, `|`, |
| `^`, `<<`, `>>`, and `>>>` for cases where a `Checked` object is the |
| right-hand side operand. If $(D rhs == WithNaN.defaultValue!Rhs), returns |
| $(D WithNaN.defaultValue!(typeof(lhs + rhs))) without evaluating the |
| operand. Otherwise, evaluates the operand. If evaluation does not overflow, |
| returns the result. Otherwise, returns $(D WithNaN.defaultValue!(typeof(lhs |
| + rhs))). |
| |
| Params: |
| x = The operator symbol |
| lhs = The left-hand side operand |
| rhs = The right-hand side operand (`Rhs` is the first argument to `Checked`) |
| |
| Returns: If $(D rhs != WithNaN.defaultValue!Rhs) and the operator does not |
| overflow, the function returns the same result as the built-in operator. In |
| all other cases, returns $(D WithNaN.defaultValue!(typeof(lhs + rhs))). |
| */ |
| auto hookOpBinaryRight(string x, L, R)(L lhs, R rhs) |
| { |
| alias Result = typeof(lhs + rhs); |
| if (rhs != defaultValue!R) |
| { |
| bool error; |
| auto result = opChecked!x(lhs, rhs, error); |
| if (!error) return result; |
| } |
| return defaultValue!Result; |
| } |
| /// |
| @safe unittest |
| { |
| Checked!(int, WithNaN) x; |
| assert((1 + x).isNaN); |
| x = 100; |
| assert(!(1 + x).isNaN); |
| } |
| |
| /** |
| |
| Defines hooks for binary operators `+=`, `-=`, `*=`, `/=`, `%=`, `^^=`, |
| `&=`, `|=`, `^=`, `<<=`, `>>=`, and `>>>=` for cases where a `Checked` |
| object is the left-hand side operand. If $(D lhs == |
| WithNaN.defaultValue!Lhs), no action is carried. Otherwise, evaluates the |
| operand. If evaluation does not overflow and fits in `Lhs` without loss of |
| information or change of sign, sets `lhs` to the result. Otherwise, sets |
| `lhs` to `WithNaN.defaultValue!Lhs`. |
| |
| Params: |
| x = The operator symbol (without the `=`) |
| lhs = The left-hand side operand (`Lhs` is the first argument to `Checked`) |
| rhs = The right-hand side operand |
| |
| Returns: `void` |
| */ |
| void hookOpOpAssign(string x, L, R)(ref L lhs, R rhs) |
| { |
| if (lhs == defaultValue!L) |
| return; |
| bool error; |
| auto temp = opChecked!x(lhs, rhs, error); |
| lhs = error |
| ? defaultValue!L |
| : hookOpCast!L(temp); |
| } |
| |
| /// |
| @safe unittest |
| { |
| Checked!(int, WithNaN) x; |
| x += 4; |
| assert(x.isNaN); |
| x = 0; |
| x += 4; |
| assert(!x.isNaN); |
| x += int.max; |
| assert(x.isNaN); |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| auto x1 = Checked!(int, WithNaN)(); |
| assert(x1.isNaN); |
| assert(x1.get == int.min); |
| assert(x1 != x1); |
| assert(!(x1 < x1)); |
| assert(!(x1 > x1)); |
| assert(!(x1 == x1)); |
| ++x1; |
| assert(x1.isNaN); |
| assert(x1.get == int.min); |
| --x1; |
| assert(x1.isNaN); |
| assert(x1.get == int.min); |
| x1 = 42; |
| assert(!x1.isNaN); |
| assert(x1 == x1); |
| assert(x1 <= x1); |
| assert(x1 >= x1); |
| static assert(x1.min == int.min + 1); |
| x1 += long(int.max); |
| } |
| |
| /** |
| Queries whether a $(D Checked!(T, WithNaN)) object is not a number (NaN). |
| |
| Params: x = the `Checked` instance queried |
| |
| Returns: `true` if `x` is a NaN, `false` otherwise |
| */ |
| bool isNaN(T)(const Checked!(T, WithNaN) x) |
| { |
| return x.get == x.init.get; |
| } |
| |
| /// |
| @safe unittest |
| { |
| auto x1 = Checked!(int, WithNaN)(); |
| assert(x1.isNaN); |
| x1 = 1; |
| assert(!x1.isNaN); |
| x1 = x1.init; |
| assert(x1.isNaN); |
| } |
| |
| @safe unittest |
| { |
| void test1(T)() |
| { |
| auto x1 = Checked!(T, WithNaN)(); |
| assert(x1.isNaN); |
| assert(x1.get == int.min); |
| assert(x1 != x1); |
| assert(!(x1 < x1)); |
| assert(!(x1 > x1)); |
| assert(!(x1 == x1)); |
| assert(x1.get == int.min); |
| auto x2 = Checked!(T, WithNaN)(42); |
| assert(!x2.isNaN); |
| assert(x2 == x2); |
| assert(x2 <= x2); |
| assert(x2 >= x2); |
| static assert(x2.min == T.min + 1); |
| } |
| test1!int; |
| test1!(const int); |
| test1!(immutable int); |
| |
| void test2(T)() |
| { |
| auto x1 = Checked!(T, WithNaN)(); |
| assert(x1.get == T.min); |
| assert(x1 != x1); |
| assert(!(x1 < x1)); |
| assert(!(x1 > x1)); |
| assert(!(x1 == x1)); |
| ++x1; |
| assert(x1.get == T.min); |
| --x1; |
| assert(x1.get == T.min); |
| x1 = 42; |
| assert(x1 == x1); |
| assert(x1 <= x1); |
| assert(x1 >= x1); |
| static assert(x1.min == T.min + 1); |
| x1 += long(T.max); |
| } |
| test2!int; |
| } |
| |
| @safe unittest |
| { |
| alias Smart(T) = Checked!(Checked!(T, ProperCompare), WithNaN); |
| Smart!int x1; |
| assert(x1 != x1); |
| x1 = -1; |
| assert(x1 < 1u); |
| auto x2 = Smart!(const int)(42); |
| } |
| |
| // Saturate |
| /** |
| |
| Hook that implements $(I saturation), i.e. any arithmetic operation that would |
| overflow leaves the result at its extreme value (`min` or `max` depending on the |
| direction of the overflow). |
| |
| Saturation is not sticky; if a value reaches its saturation value, another |
| operation may take it back to normal range. |
| |
| */ |
| struct Saturate |
| { |
| static: |
| /** |
| |
| Implements saturation for operators `+=`, `-=`, `*=`, `/=`, `%=`, `^^=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, |
| and `>>>=`. This hook is called if the result of the binary operation does |
| not fit in `Lhs` without loss of information or a change in sign. |
| |
| Params: |
| Rhs = The right-hand side type in the assignment, after the operation has |
| been computed |
| bound = The bound being violated |
| |
| Returns: `Lhs.max` if $(D rhs >= 0), `Lhs.min` otherwise. |
| |
| */ |
| T onLowerBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| return bound; |
| } |
| /// ditto |
| T onUpperBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| return bound; |
| } |
| /// |
| @safe unittest |
| { |
| auto x = checked!Saturate(short(100)); |
| x += 33000; |
| assert(x == short.max); |
| x -= 70000; |
| assert(x == short.min); |
| } |
| |
| /** |
| |
| Implements saturation for operators `+`, `-` (unary and binary), `*`, `/`, |
| `%`, `^^`, `&`, `|`, `^`, `<<`, `>>`, and `>>>`. |
| |
| For unary `-`, `onOverflow` is called if $(D lhs == Lhs.min) and `Lhs` is a |
| signed type. The function returns `Lhs.max`. |
| |
| For binary operators, the result is as follows: $(UL $(LI `Lhs.max` if the |
| result overflows in the positive direction, on division by `0`, or on |
| shifting right by a negative value) $(LI `Lhs.min` if the result overflows |
| in the negative direction) $(LI `0` if `lhs` is being shifted left by a |
| negative value, or shifted right by a large positive value)) |
| |
| Params: |
| x = The operator involved in the `opAssign` operation |
| Lhs = The left-hand side of the operator (`Lhs` is the first argument to |
| `Checked`) |
| Rhs = The right-hand side type in the operator |
| |
| Returns: The saturated result of the operator. |
| |
| */ |
| typeof(~Lhs()) onOverflow(string x, Lhs)(Lhs lhs) |
| { |
| static assert(x == "-" || x == "++" || x == "--"); |
| return x == "--" ? Lhs.min : Lhs.max; |
| } |
| /// ditto |
| typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) |
| { |
| static if (x == "+") |
| return rhs >= 0 ? Lhs.max : Lhs.min; |
| else static if (x == "*") |
| return (lhs >= 0) == (rhs >= 0) ? Lhs.max : Lhs.min; |
| else static if (x == "^^") |
| return lhs > 0 || !(rhs & 1) ? Lhs.max : Lhs.min; |
| else static if (x == "-") |
| return rhs >= 0 ? Lhs.min : Lhs.max; |
| else static if (x == "/" || x == "%") |
| return Lhs.max; |
| else static if (x == "<<") |
| return rhs >= 0 ? Lhs.max : 0; |
| else static if (x == ">>" || x == ">>>") |
| return rhs >= 0 ? 0 : Lhs.max; |
| else |
| static assert(false); |
| } |
| /// |
| @safe unittest |
| { |
| assert(checked!Saturate(int.max) + 1 == int.max); |
| assert(checked!Saturate(100) ^^ 10 == int.max); |
| assert(checked!Saturate(-100) ^^ 10 == int.max); |
| assert(checked!Saturate(100) / 0 == int.max); |
| assert(checked!Saturate(100) << -1 == 0); |
| assert(checked!Saturate(100) << 33 == int.max); |
| assert(checked!Saturate(100) >> -1 == int.max); |
| assert(checked!Saturate(100) >> 33 == 0); |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| auto x = checked!Saturate(int.max); |
| ++x; |
| assert(x == int.max); |
| --x; |
| assert(x == int.max - 1); |
| x = int.min; |
| assert(-x == int.max); |
| x -= 42; |
| assert(x == int.min); |
| assert(x * -2 == int.max); |
| } |
| |
| /* |
| Yields `true` if `T1` is "value convertible" (by C's "value preserving" rule, |
| see $(HTTP c-faq.com/expr/preservingrules.html)) 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 |
| ); |
| |
| /** |
| |
| 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. |
| |
| Params: |
| x = The binary operator involved, e.g. `/` |
| lhs = The left-hand side of the operator |
| rhs = The right-hand side of the operator |
| overflow = The overflow indicator (assigned `true` in case there's an error) |
| |
| Returns: |
| The result of the operation, which is the same as the built-in operator |
| */ |
| typeof(mixin(x == "cmp" ? "0" : ("L() " ~ x ~ " R()"))) |
| opChecked(string x, L, R)(const L lhs, const R rhs, ref bool overflow) |
| if (isIntegral!L && isIntegral!R) |
| { |
| static if (x == "cmp") |
| alias Result = int; |
| else |
| alias Result = typeof(mixin("L() " ~ x ~ " R()")); |
| |
| import core.checkedint : addu, adds, subs, muls, subu, mulu; |
| import std.algorithm.comparison : among; |
| static if (x == "==") |
| { |
| alias C = typeof(lhs + rhs); |
| 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; |
| } |
| overflow = true; |
| return true; |
| } |
| } |
| else static if (x == "cmp") |
| { |
| alias C = typeof(lhs + rhs); |
| static if (!valueConvertible!(L, C) || !valueConvertible!(R, C)) |
| { |
| static assert(isUnsigned!C); |
| static assert(isUnsigned!L != isUnsigned!R); |
| if (!isUnsigned!L && lhs < 0) |
| { |
| overflow = true; |
| return -1; |
| } |
| if (!isUnsigned!R && rhs < 0) |
| { |
| overflow = true; |
| return 1; |
| } |
| } |
| return lhs < rhs ? -1 : lhs > rhs; |
| } |
| else static if (x.among("<<", ">>", ">>>")) |
| { |
| // Handle shift separately from all others. The test below covers |
| // negative rhs as well. |
| import std.conv : unsigned; |
| 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) && x == "/") |
| { |
| 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 Result(0); |
| } |
| |
| /// |
| @safe unittest |
| { |
| bool overflow; |
| assert(opChecked!"+"(const 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); |
| } |
| |
| /// |
| @safe 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); |
| } |
| |
| @safe 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); |
| } |
| |
| @safe 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); |
| overflow = false; |
| assert(opChecked!"cmp"(0u, -6, overflow) == 1 && overflow); |
| overflow = false; |
| assert(opChecked!"|"(1, 2, overflow) == 3 && !overflow); |
| } |
| |
| /* |
| Exponentiation function used by the implementation of operator `^^`. |
| */ |
| 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; |
| } |
| |
| @safe 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"); |
| } |
| T onLowerBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| ++calls; |
| return cast(T) rhs; |
| } |
| T onUpperBound(Rhs, T)(Rhs rhs, T bound) |
| { |
| ++calls; |
| return cast(T) 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!(const int, void)(42), y = Checked!(immutable 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.get << x1.get); |
| assert(x1.hook.calls == 2); |
| x1 = int.min; |
| assert(x1 - 1 == int.max); |
| assert(x1.hook.calls == 3); |
| |
| 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 |
| @safe unittest |
| { |
| auto x1 = Checked!(int, CountOverflows)(3); |
| assert((x1 += 2) == 5); |
| x1 *= 2_000_000_000L; |
| assert(x1.hook.calls == 1); |
| x1 *= -2_000_000_000L; |
| assert(x1.hook.calls == 2); |
| |
| 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 |
| @safe unittest |
| { |
| Checked!(int, void) x; |
| x = 42; |
| assert(x.get == 42); |
| x = x; |
| assert(x.get == 42); |
| x = short(43); |
| assert(x.get == 43); |
| x = ushort(44); |
| assert(x.get == 44); |
| } |
| |
| @safe 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)).get == 42); |
| assert(Checked!(int, void)(ushort(42)).get == 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 != x1); |
| // For coverage: lhs has no hookOpEquals, rhs does |
| assert(Checked!(uint, void)(100u) != x2); |
| // For coverage: different types, neither has a hookOpEquals |
| assert(Checked!(uint, void)(100u) == Checked!(int, void*)(100)); |
| assert(x2.hook.calls == 0); |
| 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); |
|