blob: 635c4207fcdd003f4bfb925daf03ddc6bb948ca1 [file] [log] [blame]
// Written in the D programming language.
/**
$(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.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`.)
)
$(TR $(TD `hookToHash`) $(TD If defined, $(D hook.hookToHash(payload))
(where `payload` is a reference to the value wrapped by Checked) is forwarded
to when `toHash` is called on a Checked type. Custom hashing can be implemented
in a `Hook`, otherwise the built-in hashing is used.)
)
)
Source: $(PHOBOSSRC std/checkedint.d)
*/
module std.checkedint;
import std.traits : isFloatingPoint, isIntegral, isNumeric, isUnsigned, Unqual;
///
@safe 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]);
}
/// `Saturate` stops at an overflow
@safe unittest
{
auto x = (cast(byte) 127).checked!Saturate;
assert(x == 127);
x++;
assert(x == 127);
}
/// `WithNaN` has a special "Not a Number" (NaN) value akin to the homonym value reserved for floating-point values
@safe unittest
{
auto x = 100.checked!WithNaN;
assert(x == 100);
x /= 0;
assert(x.isNaN);
}
/// `ProperCompare` fixes the comparison operators ==, !=, <, <=, >, and >= to return correct results
@safe unittest
{
uint x = 1;
auto y = x.checked!ProperCompare;
assert(x < -1); // built-in comparison
assert(y > -1); // ProperCompare
}
/// `Throw` fails every incorrect operation by throwing an exception
@safe unittest
{
import std.exception : assertThrown;
auto x = -1.checked!Throw;
assertThrown(x / 0);
assertThrown(x + int.min);
assertThrown(x == uint.max);
}
/**
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`.
Params:
T = type that is wrapped in the `Checked` type
Hook = hook type that customizes the behavior of the `Checked` type
*/
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.format.spec : FormatSpec;
import std.range.primitives : isInputRange, ElementType;
import std.traits : hasMember, isSomeChar;
/**
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);
///
@safe unittest
{
assert(Checked!short.min == -32768);
assert(Checked!(short, WithNaN).min == -32767);
assert(Checked!(uint, WithNaN).max == uint.max - 1);
}
}
else
{
/// ditto
enum Checked!(T, Hook) min = Checked(T.min);
}
static if (hasMember!(Hook, "max"))
{
/// ditto
enum Checked!(T, Hook) max = Checked(Hook.max!T);
}
else
{
/// ditto
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;
}
///
@safe 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.
Params:
rhs = The value to assign
Returns:
A reference to `this`
*/
ref Checked opAssign(U)(U rhs) return
if (is(typeof(Checked!(T, Hook)(rhs))))
{
static if (isIntegral!U)
payload = rhs;
else
payload = rhs.payload;
return this;
}
///
@safe unittest
{
Checked!long a;
a = 42L;
assert(a == 42);
a = 4242;
assert(a == 4242);
}
///
@safe unittest
{
Checked!long a, b;
a = b = 3;
assert(a == 3 && b == 3);
}
/**
Construct from a decimal string. The conversion follows the same rules as
$(REF to, std, conv) converting a string to the wrapped `T` type.
Params:
str = an $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
of characters
*/
this(Range)(Range str)
if (isInputRange!Range && isSomeChar!(ElementType!Range))
{
import std.conv : to;
this(to!T(str));
}
/**
$(REF to, std, conv) can convert a string to a `Checked!T`:
*/
@system unittest
{
import std.conv : to;
const a = to!long("1234");
const b = to!(Checked!long)("1234");
assert(a == b);
}
// opCast
/**
Casting operator to integral, `bool`, or floating point type.
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.
Params:
U = The type to cast to
Returns:
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`.
*/
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);
}
}
///
@safe unittest
{
assert(cast(uint) checked(42) == 42);
assert(cast(uint) checked!WithNaN(-42) == uint.max);
}
// opEquals
/**
Compares `this` against `rhs` for equality.
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.
Params:
rhs = Right-hand side to compare for equality
Returns:
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.
*/
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
{
import std.traits : isUnsigned;
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);
}
// toHash
/**
Generates a hash for `this`. If `Hook` defines `hookToHash`, the call
immediately returns `hook.hookToHash(payload)`. If `Hook` does not
implement `hookToHash`, but it has state, a hash will be generated for
the `Hook` using the built-in function and it will be xored with the
hash of the `payload`.
Returns:
The hash of `this` instance.
*/
size_t toHash() const nothrow @safe
{
static if (hasMember!(Hook, "hookToHash"))
{
return hook.hookToHash(payload);
}
else static if (stateSize!Hook > 0)
{
static if (hasMember!(typeof(payload), "toHash"))
{
return payload.toHash() ^ hashOf(hook);
}
else
{
return hashOf(payload) ^ hashOf(hook);
}
}
else static if (hasMember!(typeof(payload), "toHash"))
{
return payload.toHash();
}
else
{
return .hashOf(payload);
}
}
/// ditto
size_t toHash(this _)() shared const nothrow @safe
{
import core.atomic : atomicLoad, MemoryOrder;
static if (is(typeof(this.payload.atomicLoad!(MemoryOrder.acq)) P))
{
auto payload = __ctfe ? cast(P) this.payload
: this.payload.atomicLoad!(MemoryOrder.acq);
}
else
{
alias payload = this.payload;
}
static if (hasMember!(Hook, "hookToHash"))
{
return hook.hookToHash(payload);
}
else static if (stateSize!Hook > 0)
{
static if (hasMember!(typeof(payload), "toHash"))
{
return payload.toHash() ^ hashOf(hook);
}
else
{
return hashOf(payload) ^ hashOf(hook);
}
}
else static if (hasMember!(typeof(payload), "toHash"))
{
return payload.toHash();
}
else
{
return .hashOf(payload);
}
}
/**
Writes a string representation of this to a `sink`.
Params:
sink = A `Char` accepting
$(REF_ALTTEXT output range, isOutputRange, std,range,primitives).
fmt = A $(REF FormatSpec, std, format) which controls how this
is formatted.
*/
void toString(Writer, Char)(scope ref Writer sink, scope const ref FormatSpec!Char fmt) const
{
import std.format.write : formatValue;
if (fmt.spec == 's')
return formatValue(sink, this, fmt);
else
return formatValue(sink, payload, fmt);
}
/**
`toString` is rarely directly invoked; the usual way of using it is via
$(REF format, std, format):
*/
@system unittest
{
import std.format;
assert(format("%04d", checked(15)) == "0015");
assert(format("0x%02x", checked(15)) == "0x0f");
}
// 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.
Params:
rhs = The right-hand side operand
U = either the type of `rhs` or the underlying type
if `rhs` is a `Checked` instance
Hook1 = If `rhs` is a `Checked` instance, `Hook1` represents
the instance's behavior hook
Returns:
The result of `hookOpCmp` if `hook` defines `hookOpCmp`. If
`U` is an instance of `Checked` and `hook` does not define
`hookOpCmp`, result of `rhs.hook.hookOpCmp` is returned.
If none of the instances specify the behavior via `hookOpCmp`,
`-1` is returned if `lhs` is lesser than `rhs`, `1` if `lhs`
is greater than `rhs` and `0` on equality.
*/
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
{
import std.traits : isUnsigned;
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)) @safe 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.
Params:
op = The unary operator
Returns:
A `Checked` instance representing the result of the unary
operation
*/
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.
Params:
op = The binary operator
rhs = The right hand side operand
U = If `rhs` is a `Checked` instance, `U` represents
the underlying instance type
Hook1 = If `rhs` is a `Checked` instance, `Hook1` represents
the instance's behavior hook
Returns:
A `Checked` instance representing the result of the binary
operation
*/
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)) @safe 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.
Params:
op = The binary operator
lhs = The left hand side operand
Returns:
A `Checked` instance representing the result of the binary
operation
*/
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(lhs, payload);
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)) @safe 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)).
If the right-hand side is also a Checked but with a different hook or
underlying type, the hook and underlying type of this Checked takes
precedence.
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;
}
/// ditto
ref Checked opOpAssign(string op, Rhs)(const Rhs rhs) return
if (is(Rhs == Checked!(RhsT, RhsHook), RhsT, RhsHook))
{
return opOpAssign!(op, typeof(rhs.payload))(rhs.payload);
}
///
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);
}
}
///
@safe @nogc pure nothrow unittest
{
// Hook that ignores all problems.
static struct Ignore
{
@nogc nothrow pure @safe static:
Dst onBadCast(Dst, Src)(Src src) { return cast(Dst) src; }
Lhs onLowerBound(Rhs, T)(Rhs rhs, T bound) { return cast(T) rhs; }
T onUpperBound(Rhs, T)(Rhs rhs, T bound) { return cast(T) rhs; }
bool hookOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { return lhs == rhs; }
int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { return (lhs > rhs) - (lhs < rhs); }
typeof(~Lhs()) onOverflow(string x, Lhs)(ref Lhs lhs) { return mixin(x ~ "lhs"); }
typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs)
{
static if (x == "/")
return typeof(lhs / rhs).min;
else
return mixin("lhs" ~ x ~ "rhs");
}
}
auto x = Checked!(int, Ignore)(5) + 7;
}
/**
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`).
Params:
Hook = type that customizes the behavior, by default `Abort`
T = type represetinfg the underlying represantion of the `Checked` instance
value = the actual value of the representation
Returns:
A `Checked` instance customized by the provided `Hook` and `value`
*/
Checked!(T, Hook) checked(Hook = Abort, T)(const T value)
if (is(typeof(Checked!(T, Hook)(value))))
{
return Checked!(T, Hook)(value);
}
///
@safe 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);
}
@system unittest
{
// https://issues.dlang.org/show_bug.cgi?id=21758
assert(4 * checked(5L) == 20);
assert(20 / checked(5L) == 4);
assert(2 ^^ checked(3L) == 8);
assert(12 % checked(5L) == 2);
assert((0xff & checked(3L)) == 3);
assert((0xf0 | checked(3L)) == 0xf3);
assert((0xff ^ checked(3L)) == 0xfc);
}
// 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 = Souce operand
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);
}
}
///
@safe 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
{
/**
Params:
f = format specifier
vals = actual values for the format specifier
*/
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 = source operand
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.
Throws:
`CheckFailure` on bad cast
*/
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.
Throws:
`CheckFailure` on overflow
*/
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.
Throws:
`CheckFailure` on overflow
*/
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 : writefln;
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)
{
trustedStderr.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(T) rhs`
*/
T onLowerBound(Rhs, T)(Rhs rhs, T bound)
{
trustedStderr.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)
{
trustedStderr.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)
{
trustedStderr.writefln("Erroneous comparison: %s(%s) == %s(%s)",
Lhs.stringof, lhs, Rhs.stringof, rhs);
return lhs == rhs;
}
return result;
}
///
@safe 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)
{
trustedStderr.writefln("Erroneous ordering comparison: %s(%s) and %s(%s)",
Lhs.stringof, lhs, Rhs.stringof, rhs);
return lhs < rhs ? -1 : lhs > rhs;
}
return result;
}
///
@safe 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)
{
trustedStderr.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)
{
trustedStderr.writefln("Overflow on binary operator: %s(%s) %s %s(%s)",
Lhs.stringof, lhs, x, Rhs.stringof, rhs);
static if (x == "/") // Issue 20743: mixin below would cause SIGFPE on POSIX
return typeof(lhs / rhs).min; // or EXCEPTION_INT_OVERFLOW on Windows
else
return mixin("lhs" ~ x ~ "rhs");
}
// This is safe because we do not assign to the reference returned by
// `stderr`. The ability for the caller to do that is why `stderr` is not
// safe in the general case.
private @property auto ref trustedStderr() @trusted
{
import std.stdio : stderr;
return stderr;
}
}
///
@safe 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;
}
@system unittest
{
auto a = checked!Warn(int.min);
auto b = checked!Warn(-1);
auto x = checked!Abort(int.min);
auto y = checked!Abort(-1);
// Temporarily redirect output to stderr to make sure we get the right output.
import std.file : exists, remove;
import std.process : uniqueTempPath;
import std.stdio : stderr;
auto tmpname = uniqueTempPath;
scope(exit) if (exists(tmpname)) remove(tmpname);
auto t = stderr;
stderr.open(tmpname, "w");
// Open a new scope to minimize code ran with stderr redirected.
{
scope(exit) stderr = t;
assert(a / b == a * b);
import std.exception : assertThrown;
import core.exception : AssertError;
assertThrown!AssertError(x / y);
}
import std.file : readText;
import std.ascii : newline;
auto witness = readText(tmpname);
auto expected =
"Overflow on binary operator: int(-2147483648) / const(int)(-1)" ~ newline ~
"Overflow on binary operator: int(-2147483648) * const(int)(-1)" ~ newline ~
"Overflow on binary operator: int(-2147483648) / const(int)(-1)" ~ newline;
assert(witness == expected, "'" ~ witness ~ "'");
}
// https://issues.dlang.org/show_bug.cgi?id=22249
@safe unittest
{
alias _ = Warn.onLowerBound!(int, int);
}
// 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.traits : 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 `T.max` for signed integrals, $(D
T.max - 1) for unsigned integrals. The minimum value representable is $(D
T.min + 1) for signed integrals, `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, T bound)
{
return bound;
}
/// ditto
T onUpperBound(Rhs, T)(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 type 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.
*/
auto onOverflow(string x, 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 (StdUnittest) 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)
{
++calls;
return cast(T) rhs;
}
T onUpperBound(Rhs, T)(Rhs rhs, T)
{
++calls;
return cast(T) rhs;
}
}
// opBinary
@nogc nothrow pure @safe unittest
{
static struct CountOpBinary
{
uint calls;
auto hookOpBinary(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs)
{
++calls;
return mixin("lhs" ~ op ~ "rhs");
}
}
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);
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;
void hookOpUnary(string op, T)(ref T value) if (op == "++")
{
++calls;
--value;
}
void 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);
const 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);
const Checked!(uint, void) y;
assert(y <= y);
assert(y == 0);
assert(y < x);
x = -1;
assert(x > y);
}
@nogc nothrow pure @safe unittest
{
alias cint = Checked!(int, void);
cint a = 1, b = 2;
a += b;
assert(a == cint(3));
alias ccint = Checked!(cint, Saturate);
ccint c = 14;
a += c;
assert(a == cint(17));
}
// toHash
@safe unittest
{
assert(checked(42).toHash() == checked(42).toHash());
assert(checked(12).toHash() != checked(19).toHash());
static struct Hook1
{
static size_t hookToHash(T)(T payload) nothrow @trusted
{
static if (size_t.sizeof == 4)
{
return typeid(payload).getHash(&payload) ^ 0xFFFF_FFFF;
}
else
{
return typeid(payload).getHash(&payload) ^ 0xFFFF_FFFF_FFFF_FFFF;
}
}
}
auto a = checked!Hook1(78);
auto b = checked!Hook1(78);
assert(a.toHash() == b.toHash());
assert(checked!Hook1(12).toHash() != checked!Hook1(13).toHash());
static struct Hook2
{
static if (size_t.sizeof == 4)
{
static size_t hashMask = 0xFFFF_0000;
}
else
{
static size_t hashMask = 0xFFFF_0000_FFFF_0000;
}
static size_t hookToHash(T)(T payload) nothrow @trusted
{
return typeid(payload).getHash(&payload) ^ hashMask;
}
}
auto x = checked!Hook2(1901);
auto y = checked!Hook2(1989);
assert((() nothrow @safe => x.toHash() == x.toHash())());
assert(x.toHash() == x.toHash());
assert(x.toHash() != y.toHash());
assert(checked!Hook1(1901).toHash() != x.toHash());
immutable z = checked!Hook1(1901);
immutable t = checked!Hook1(1901);
immutable w = checked!Hook2(1901);
assert(z.toHash() == t.toHash());
assert(z.toHash() != x.toHash());
assert(z.toHash() != w.toHash());
const long c = 0xF0F0F0F0;
const long d = 0xF0F0F0F0;
assert(checked!Hook1(c).toHash() != checked!Hook2(c));
assert(checked!Hook1(c).toHash() != checked!Hook1(d));
// Hook with state, does not implement hookToHash
static struct Hook3
{
ulong var1 = ulong.max;
uint var2 = uint.max;
}
assert(checked!Hook3(12).toHash() != checked!Hook3(13).toHash());
assert(checked!Hook3(13).toHash() == checked!Hook3(13).toHash());
// Hook with no state and no hookToHash, payload has its own hashing function
auto x1 = Checked!(Checked!int, ProperCompare)(123);
auto x2 = Checked!(Checked!int, ProperCompare)(123);
auto x3 = Checked!(Checked!int, ProperCompare)(144);
assert(x1.toHash() == x2.toHash());
assert(x1.toHash() != x3.toHash());
assert(x2.toHash() != x3.toHash());
// Check shared.
{
shared shared0 = checked(12345678);
shared shared1 = checked!Hook1(123456789);
shared shared2 = checked!Hook2(234567891);
shared shared3 = checked!Hook3(345678912);
assert(shared0.toHash() == hashOf(shared0));
assert(shared1.toHash() == hashOf(shared1));
assert(shared2.toHash() == hashOf(shared2));
assert(shared3.toHash() == hashOf(shared3));
}
}
///
@safe unittest
{
struct MyHook
{
static size_t hookToHash(T)(const T payload) nothrow @trusted
{
return .hashOf(payload);
}
}
int[Checked!(int, MyHook)] aa;
Checked!(int, MyHook) var = 42;
aa[var] = 100;
assert(aa[var] == 100);
int[Checked!(int, Abort)] bb;
Checked!(int, Abort) var2 = 42;
bb[var2] = 100;
assert(bb[var2] == 100);
}