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