From cecb6d89166fe62e4b0f665b8d21f15553eaee43 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Mon, 18 Jul 2016 10:26:43 -0400 Subject: [PATCH 01/26] WIP: add std.experimental.checkedint --- posix.mak | 2 +- std/experimental/checkedint.d | 1584 +++++++++++++++++++++++++++++++++ 2 files changed, 1585 insertions(+), 1 deletion(-) create mode 100644 std/experimental/checkedint.d diff --git a/posix.mak b/posix.mak index 7c1ac40a8aa..9a42ab47f4a 100644 --- a/posix.mak +++ b/posix.mak @@ -175,7 +175,7 @@ PACKAGE_std = array ascii base64 bigint bitmanip compiler complex concurrency \ outbuffer parallelism path process random signals socket stdint \ stdio stdiobase string system traits typecons typetuple uni \ uri utf uuid variant xml zip zlib -PACKAGE_std_experimental = typecons +PACKAGE_std_experimental = checkedint typecons PACKAGE_std_algorithm = comparison iteration mutation package searching setops \ sorting PACKAGE_std_container = array binaryheap dlist package rbtree slist util diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d new file mode 100644 index 00000000000..2548b14132e --- /dev/null +++ b/std/experimental/checkedint.d @@ -0,0 +1,1584 @@ +/** + +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); +} From e7be3c823d53421b55406c52bf8dc5cd5a30a8c8 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Mon, 18 Jul 2016 15:32:56 -0400 Subject: [PATCH 02/26] Add changes to winxx.mak too --- win32.mak | 2 +- win64.mak | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/win32.mak b/win32.mak index 6c4e92ebe05..54e72ec7a27 100644 --- a/win32.mak +++ b/win32.mak @@ -291,7 +291,7 @@ SRC_STD_INTERNAL_WINDOWS= \ std\internal\windows\advapi32.d SRC_STD_EXP= \ - std\experimental\typecons.d + std\experimental\checkedint.d std\experimental\typecons.d SRC_STD_EXP_ALLOC_BB= \ std\experimental\allocator\building_blocks\affix_allocator.d \ diff --git a/win64.mak b/win64.mak index df4e1abbebd..0d7b7c2a194 100644 --- a/win64.mak +++ b/win64.mak @@ -310,7 +310,7 @@ SRC_STD_INTERNAL_WINDOWS= \ std\internal\windows\advapi32.d SRC_STD_EXP= \ - std\experimental\typecons.d + std\experimental\checkedint.d std\experimental\typecons.d SRC_STD_EXP_ALLOC_BB= \ std\experimental\allocator\building_blocks\affix_allocator.d \ From 0033df3bd8ff1a60c47c73a5b1f460e7ac787cb9 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Mon, 18 Jul 2016 17:57:21 -0400 Subject: [PATCH 03/26] Add dox for ProperCompare --- std/experimental/checkedint.d | 44 ++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 2548b14132e..3d7b450d2af 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -594,9 +594,34 @@ unittest // ProperCompare /** + +Implements a 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. + */ static bool hookOpEquals(L, R)(L lhs, R rhs) { alias C = typeof(lhs + rhs); @@ -641,6 +666,19 @@ 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 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`. + */ static auto hookOpCmp(L, R)(L lhs, R rhs) { alias C = typeof(lhs + rhs); @@ -666,15 +704,19 @@ struct ProperCompare } } +/// unittest { alias opEqualsProper = ProperCompare.hookOpEquals; assert(opEqualsProper(42, 42)); assert(opEqualsProper(42u, 42)); assert(opEqualsProper(42, 42u)); - assert(!opEqualsProper(-1, uint(-1))); + assert(-1 == 4294967295u); + assert(!opEqualsProper(-1, 4294967295u)); assert(!opEqualsProper(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)); } unittest From aeb6325c9381fa5e2cf960ad07ba6ee32d7988da Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Tue, 19 Jul 2016 12:15:53 -0400 Subject: [PATCH 04/26] Review --- std/experimental/checkedint.d | 119 ++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 3d7b450d2af..feced1d1303 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -38,7 +38,7 @@ 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`, + 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 @@ -76,7 +76,6 @@ 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 @@ -86,14 +85,14 @@ help of a `Hook` type. The type wrapped must be one of the predefined integrals 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.algorithm.comparison : among; import std.traits : hasMember; import std.experimental.allocator.common : stateSize; /** The type of the integral subject to checking. */ - alias Payload = T; + alias Representation = T; // state { static if (hasMember!(Hook, "defaultValue")) @@ -144,13 +143,14 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /** 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. + 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(rhs.payload)))) + is(U == Checked!(V, W), V, W) && + is(typeof(Checked(rhs.representation)))) { static if (isIntegral!U) payload = rhs; @@ -179,15 +179,16 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) 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. + U(representation)). If that is not `true`, + `hook.onBadCast!U(representation)` 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. + are not arithmetically equal, `hook.onBadCast!U(representation)` is + returned. */ U opCast(U)() @@ -236,7 +237,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) 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. + built-in operation $(D representation == rhs) is returned. If `U` is an instance of `Checked` */ @@ -260,7 +261,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) { return hook.hookOpEquals(payload, rhs.payload); } - else static if (hasMember!(Hook1, "hookOpEquals")) + else static if (hasMember!(W, "hookOpEquals")) { return rhs.hook.hookOpEquals(rhs.payload, payload); } @@ -342,13 +343,13 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) auto r = hook.hookOpUnary!op(payload); return Checked!(typeof(r), Hook)(r); } - else static if (!isUnsigned!T && op == "-" && + else static if (isIntegral!T && !isUnsigned!T && op == "-" && hasMember!(Hook, "onOverflow")) { import core.checkedint; - alias R = typeof(-payload); + static assert(is(typeof(-payload) == typeof(payload))); bool overflow; - auto r = negs(R(payload), overflow); + auto r = negs(payload, overflow); if (overflow) r = hook.onOverflow!op(payload); return Checked(r); } @@ -425,7 +426,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /// ditto auto opBinary(string op, U, Hook1)(Checked!(U, Hook1) rhs) { - alias R = typeof(payload + rhs.payload); + alias R = typeof(representation + rhs.payload); static if (valueConvertible!(T, R) && valueConvertible!(T, R) || is(Hook == Hook1)) { @@ -526,8 +527,11 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else { static if (isUnsigned!T && !isUnsigned!R) + { // Example: ushort += int - const bad = r < 0 || r > max.payload; + import std.conv : unsigned; + const bad = unsigned(r) > max.payload; + } else // Some narrowing is afoot static if (R.min < min.payload) @@ -576,11 +580,11 @@ static: } typeof(~Lhs()) onOverflow(string op, Lhs)(Lhs lhs) { - assert(0); + assert(0, "Overflow on unary \"" ~ op ~ "\""); } typeof(Lhs() + Rhs()) onOverflow(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { - assert(0); + assert(0, "Overflow on binary \"" ~ op ~ "\""); } } @@ -733,7 +737,7 @@ unittest unittest { auto x1 = Checked!(uint, ProperCompare)(42u); - assert(x1.payload < -1); + assert(x1.representation < -1); assert(x1 > -1); } @@ -815,20 +819,34 @@ static: 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; + if (lhs != defaultValue!L) + { + bool error; + auto result = opChecked!x(lhs, rhs, error); + if (!error) return result; + } + return 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; + if (rhs != defaultValue!R) + { + bool error; + auto result = opChecked!x(lhs, rhs, error); + if (!error) return result; + } + return defaultValue!Result; } void hookOpOpAssign(string x, L, R)(ref L lhs, R rhs) { - if (lhs != defaultValue!L) mixin("lhs"~x~"=rhs;"); + if (lhs == defaultValue!L) + return; + bool error; + auto temp = opChecked!x(lhs, rhs, error); + lhs = error + ? defaultValue!L + : hookOpCast!L(temp); } } @@ -836,15 +854,15 @@ static: unittest { auto x1 = Checked!(int, WithNaN)(); - assert(x1.payload == int.min); + assert(x1.representation == int.min); assert(x1 != x1); assert(!(x1 < x1)); assert(!(x1 > x1)); assert(!(x1 == x1)); ++x1; - assert(x1.payload == int.min); + assert(x1.representation == int.min); --x1; - assert(x1.payload == int.min); + assert(x1.representation == int.min); x1 = 42; assert(x1 == x1); assert(x1 <= x1); @@ -910,16 +928,17 @@ additional work. */ typeof(L() + R()) opChecked(string x, L, R)(const L lhs, const R rhs, - ref bool overflow) + ref bool error) if (isIntegral!L && isIntegral!R) { alias Result = typeof(lhs + rhs); import core.checkedint; - import std.algorithm : among; + import std.algorithm.comparison : among; 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"); } @@ -931,7 +950,7 @@ if (isIntegral!L && isIntegral!R) else static if (x == "^^") { // Exponentiation is weird, handle separately - return pow(lhs, rhs, overflow); + return pow(lhs, rhs, error); } else static if (valueConvertible!(L, Result) && valueConvertible!(R, Result)) @@ -946,13 +965,13 @@ if (isIntegral!L && isIntegral!R) { static if (isUnsigned!Result) alias impl = addu; else alias impl = adds; - return impl(Result(lhs), Result(rhs), overflow); + return impl(Result(lhs), Result(rhs), error); } else static if (x == "-") { static if (isUnsigned!Result) alias impl = subu; else alias impl = subs; - return impl(Result(lhs), Result(rhs), overflow); + return impl(Result(lhs), Result(rhs), error); } else static if (x == "*") { @@ -963,7 +982,7 @@ if (isIntegral!L && isIntegral!R) } static if (isUnsigned!Result) alias impl = mulu; else alias impl = muls; - return impl(Result(lhs), Result(rhs), overflow); + return impl(Result(lhs), Result(rhs), error); } else static if (x == "/" || x == "%") { @@ -986,14 +1005,14 @@ if (isIntegral!L && isIntegral!R) static if (!isUnsigned!L) { if (lhs < 0) - return subu(Result(rhs), Result(-lhs), overflow); + return subu(Result(rhs), Result(-lhs), error); } else static if (!isUnsigned!R) { if (rhs < 0) - return subu(Result(lhs), Result(-rhs), overflow); + return subu(Result(lhs), Result(-rhs), error); } - return addu(Result(lhs), Result(rhs), overflow); + return addu(Result(lhs), Result(rhs), error); } else static if (x == "-") { @@ -1004,9 +1023,9 @@ if (isIntegral!L && isIntegral!R) else static if (!isUnsigned!R) { if (rhs < 0) - return addu(Result(lhs), Result(-rhs), overflow); + return addu(Result(lhs), Result(-rhs), error); } - return subu(Result(lhs), Result(rhs), overflow); + return subu(Result(lhs), Result(rhs), error); } else static if (x == "*") { @@ -1018,7 +1037,7 @@ if (isIntegral!L && isIntegral!R) { if (rhs < 0) goto fail; } - return mulu(Result(lhs), Result(rhs), overflow); + return mulu(Result(lhs), Result(rhs), error); } else static if (x == "/" || x == "%") { @@ -1036,7 +1055,7 @@ if (isIntegral!L && isIntegral!R) } debug assert(false); fail: - overflow = true; + error = true; return 0; } @@ -1255,7 +1274,7 @@ version(unittest) private struct CountOpBinary assert(x1.hook.calls == 1); assert(x1 << 2 == 42 << 2); assert(x1.hook.calls == 1); - assert(x1 << 42 == x1.payload << x1.payload); + assert(x1 << 42 == x1.representation << x1.representation); assert(x1.hook.calls == 2); auto x2 = Checked!(int, CountOpBinary)(42); @@ -1343,13 +1362,13 @@ unittest { Checked!(int, void) x; x = 42; - assert(x.payload == 42); + assert(x.representation == 42); x = x; - assert(x.payload == 42); + assert(x.representation == 42); x = short(43); - assert(x.payload == 43); + assert(x.representation == 43); x = ushort(44); - assert(x.payload == 44); + assert(x.representation == 44); } unittest @@ -1357,8 +1376,8 @@ 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); + assert(Checked!(short, void)(short(42)).representation == 42); + assert(Checked!(int, void)(ushort(42)).representation == 42); } // opCast From 2ca88721011cca4b185b88c8c3c0a0fd224d67d8 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Mon, 15 Aug 2016 13:40:05 -0400 Subject: [PATCH 05/26] Review --- std/experimental/checkedint.d | 126 +++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index feced1d1303..98127ad07e6 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -12,13 +12,14 @@ 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. +`assert(0)` (i.e. throws an `Error` in debug mode or aborts execution in release +mode) 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 @@ -32,9 +33,9 @@ This module provides a few predefined hooks (below) that add useful behavior to $(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 `Abort` fails every incorrect operation with a message to `stderr` followed +by a call to `abort()`. It is the default second parameter, i.e. `Checked!short` +is the same as $(D Checked!(short, Abort)).) $(LI `ProperCompare` fixes the comparison operators `==`, `!=`, `<`, `<=`, `>`, and `>=` to return correct results in all circumstances, at a slight cost in @@ -58,10 +59,10 @@ $(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 +`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 `Croak` will +Checked!(Checked!(int, ProperCompare))) is meaningless because `Abort` will intercept comparison and will fail without giving `ProperCompare` a chance to intervene.) @@ -77,12 +78,29 @@ intercepts comparisons before the numbers involved are tested for NaN.) module std.experimental.checkedint; import std.traits : isFloatingPoint, isIntegral, isNumeric, isUnsigned, Unqual; +/// +unittest +{ + int[] addAndMerge(int[] a, int[] b, int offset) + { + // Aborts on overflow on size computation + auto r = new int[(Checked!size_t(a.length) + b.length).representation]; + // Aborts on overflow on element computation + foreach (i; 0 .. a.length) + r[i] = (a[i] + Checked!int(offset)).representation; + foreach (i; 0 .. b.length) + r[i + a.length] = (b[i] + Checked!int(offset)).representation; + return r; + } + assert(addAndMerge([1, 2, 3], [4, 5], -1) == [0, 1, 2, 3, 4]); +} + /** Checked integral type wraps an integral `T` and customizes its behavior with the help of a `Hook` type. The type wrapped must be one of the predefined integrals (unqualified), or another instance of `Checked`. */ -struct Checked(T, Hook = Croak) +struct Checked(T, Hook = Abort) if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) { import std.algorithm.comparison : among; @@ -307,7 +325,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) auto opCmp(U, Hook1)(Checked!(U, Hook1) rhs) { alias R = typeof(payload + rhs.payload); - static if (valueConvertible!(T, R) && valueConvertible!(T, R)) + static if (valueConvertible!(T, R) && valueConvertible!(U, R)) { return payload < rhs.payload ? -1 : payload > rhs.payload; } @@ -392,7 +410,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) if (isIntegral!Rhs || isFloatingPoint!Rhs || is(Rhs == bool)) { alias R = typeof(payload + rhs); - static assert(is(typeof(mixin("payload"~op~"rhs")) == R)); + static assert(is(typeof(mixin("payload" ~ op ~ "rhs")) == R)); static if (isIntegral!R) alias Result = Checked!(R, Hook); else alias Result = R; @@ -403,11 +421,11 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } else static if (is(Rhs == bool)) { - return mixin("this"~op~"ubyte(rhs)"); + return mixin("this" ~ op ~ "ubyte(rhs)"); } else static if (isFloatingPoint!Rhs) { - return mixin("payload"~op~"rhs"); + return mixin("payload" ~ op ~ "rhs"); } else static if (hasMember!(Hook, "onOverflow")) { @@ -419,7 +437,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else { // Default is built-in behavior - return Result(mixin("payload"~op~"rhs")); + return Result(mixin("payload" ~ op ~ "rhs")); } } @@ -427,11 +445,11 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) auto opBinary(string op, U, Hook1)(Checked!(U, Hook1) rhs) { alias R = typeof(representation + rhs.payload); - static if (valueConvertible!(T, R) && valueConvertible!(T, R) || + static if (valueConvertible!(T, R) && valueConvertible!(U, R) || is(Hook == Hook1)) { // Delegate to lhs - return mixin("this"~op~"rhs.payload"); + return mixin("this" ~ op ~ "rhs.payload"); } else static if (hasMember!(Hook, "hookOpBinary")) { @@ -440,23 +458,23 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else static if (hasMember!(Hook1, "hookOpBinary")) { // Delegate to rhs - return mixin("this.payload"~op~"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"); + return mixin("this" ~ op ~ "rhs.payload"); } else static if (hasMember!(Hook1, "onOverflow") && !hasMember!(Hook, "onOverflow")) { // Delegate to rhs - return mixin("this.payload"~op~"rhs"); + return mixin("this.payload" ~ op ~ "rhs"); } else { - static assert(0, "Conflict between lhs and rhs hooks," + static assert(0, "Conflict between lhs and rhs hooks," ~ " use .representation on one side to disambiguate."); } } @@ -479,11 +497,11 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } else static if (is(Lhs == bool)) { - return mixin("ubyte(lhs)"~op~"this"); + return mixin("ubyte(lhs)" ~ op ~ "this"); } else static if (isFloatingPoint!Lhs) { - return mixin("lhs"~op~"payload"); + return mixin("lhs" ~ op ~ "payload"); } else static if (hasMember!(Hook, "onOverflow")) { @@ -495,7 +513,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else { // Default is built-in behavior - auto r = mixin("lhs"~op~"T(payload)"); + auto r = mixin("lhs" ~ op ~ "T(payload)"); return Checked!(typeof(r), Hook)(r); } } @@ -506,7 +524,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) 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 assert(is(typeof(mixin("payload" ~ op ~ "=rhs")) == T)); static if (hasMember!(Hook, "hookOpOpAssign")) { @@ -515,7 +533,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else { alias R = typeof(payload + rhs); - auto r = mixin("this"~op~"rhs").payload; + auto r = mixin("this" ~ op ~ "rhs").payload; static if (valueConvertible!(R, T) || !hasMember!(Hook, "onBadOpOpAssign") || @@ -556,41 +574,55 @@ unittest assert(Checked!(ubyte, void)(ubyte(22)).representation == 22); } +// Abort /** Force all overflows to fail with `assert(0)`. */ -struct Croak +struct Abort { + private static void abort(string msg) + { + import std.stdio : stderr; + stderr.writeln(msg); + import core.stdc.stdlib : abort; + abort(); + } static: Dst onBadCast(Dst, Src)(Src src) { - assert(0, "Bad cast"); + abort("Bad cast"); + assert(0); } Lhs onBadOpOpAssign(string x, Lhs, Rhs)(Lhs, Rhs) { - assert(0, "Bad opAssign"); + abort("Bad opAssign"); + assert(0); } bool onBadOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - assert(0, "Bad comparison for equality"); + abort("Bad comparison for equality"); + assert(0); } bool onBadOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - assert(0, "Bad comparison for ordering"); + abort("Bad comparison for ordering"); + assert(0); } typeof(~Lhs()) onOverflow(string op, Lhs)(Lhs lhs) { - assert(0, "Overflow on unary \"" ~ op ~ "\""); + abort("Overflow on unary \"" ~ op ~ "\""); + assert(0); } typeof(Lhs() + Rhs()) onOverflow(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { - assert(0, "Overflow on binary \"" ~ op ~ "\""); + abort("Overflow on binary \"" ~ op ~ "\""); + assert(0); } } unittest { - Checked!(int, Croak) x; + Checked!(int, Abort) x; x = 42; short x1 = cast(short) x; //x += long(int.max); @@ -793,7 +825,7 @@ static: { static if (x == "-" || x == "~") { - return v != defaultValue!T ? mixin(x~"v") : v; + return v != defaultValue!T ? mixin(x ~ "v") : v; } else static if (x == "++") { @@ -940,12 +972,12 @@ if (isIntegral!L && isIntegral!R) // negative rhs as well. import std.conv : unsigned; if (unsigned(rhs) > 8 * Result.sizeof) goto fail; - return mixin("lhs"~x~"rhs"); + return mixin("lhs" ~ x ~ "rhs"); } else static if (x.among("&", "|", "^")) { // Nothing to check - return mixin("lhs"~x~"rhs"); + return mixin("lhs" ~ x ~ "rhs"); } else static if (x == "^^") { @@ -959,7 +991,7 @@ if (isIntegral!L && isIntegral!R) x.among("+", "-", "*")) { // No checks - both are value converted and result is in range - return mixin("lhs"~x~"rhs"); + return mixin("lhs" ~ x ~ "rhs"); } else static if (x == "+") { @@ -992,7 +1024,7 @@ if (isIntegral!L && isIntegral!R) if (lhs == Result.min && rhs == -1) goto fail; } if (rhs == 0) goto fail; - return mixin("lhs"~x~"rhs"); + return mixin("lhs" ~ x ~ "rhs"); } else static assert(0, x); } @@ -1049,7 +1081,7 @@ if (isIntegral!L && isIntegral!R) { if (rhs <= 0) goto fail; } - return mixin("Result(lhs)"~x~"Result(rhs)"); + return mixin("Result(lhs)" ~ x ~ "Result(rhs)"); } else static assert(0, x); } @@ -1222,17 +1254,17 @@ version(unittest) private struct CountOverflows auto onOverflow(string op, Lhs)(Lhs lhs) { ++calls; - return mixin(op~"lhs"); + return mixin(op ~ "lhs"); } auto onOverflow(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; - return mixin("lhs"~op~"rhs"); + return mixin("lhs" ~ op ~ "rhs"); } Lhs onBadOpOpAssign(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; - return mixin("lhs"~op~"=rhs"); + return mixin("lhs" ~ op ~ "=rhs"); } } @@ -1242,7 +1274,7 @@ version(unittest) private struct CountOpBinary auto hookOpBinary(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) { ++calls; - return mixin("lhs"~op~"rhs"); + return mixin("lhs" ~ op ~ "rhs"); } } From c9d9920b80436a726c1d2d7b32471ce5b523f64e Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 18 Aug 2016 14:11:25 -0400 Subject: [PATCH 06/26] Rename representation to get; add function checked; improve documentation --- std/experimental/checkedint.d | 618 ++++++++++++++++++++++++++++------ 1 file changed, 523 insertions(+), 95 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 98127ad07e6..2e77b9cbb8a 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -3,23 +3,24 @@ 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. +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 `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)` (i.e. throws an `Error` in debug mode or aborts execution in release -mode) 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. +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, $(D checked(1_000_000) * +10_000) aborts execution because the operation overflows. Also, $(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 @@ -33,19 +34,20 @@ This module provides a few predefined hooks (below) that add useful behavior to $(UL -$(LI `Abort` fails every incorrect operation with a message to `stderr` followed -by a call to `abort()`. It is the default second parameter, i.e. `Checked!short` -is the same as $(D Checked!(short, Abort)).) +$(LI $(LREF Abort) fails every incorrect operation with a message to $(REF +stderr, std, stdio) followed by a call to `std.core.abort`. It is the default +second parameter, i.e. `Checked!short` is the same as $(D Checked!(short, +Abort)). This is also the type returned by the `checked` convenience function.) -$(LI `ProperCompare` fixes the comparison operators `==`, `!=`, `<`, `<=`, `>`, +$(LI $(LREF 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 +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.) -$(LI `WithNaN` reserves a special "Not a Number" value. ) +$(LI $(LREF WithNaN) reserves a special "Not a Number" value.) ) @@ -53,7 +55,7 @@ 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: +integral can be built on top of it. Some combinations of interest include: $(UL @@ -81,18 +83,18 @@ import std.traits : isFloatingPoint, isIntegral, isNumeric, isUnsigned, Unqual; /// unittest { - int[] addAndMerge(int[] a, int[] b, int offset) + int[] concatAndAdd(int[] a, int[] b, int offset) { // Aborts on overflow on size computation - auto r = new int[(Checked!size_t(a.length) + b.length).representation]; + 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!int(offset)).representation; + r[i] = (a[i] + checked(offset)).get; foreach (i; 0 .. b.length) - r[i + a.length] = (b[i] + Checked!int(offset)).representation; + r[i + a.length] = (b[i] + checked(offset)).get; return r; } - assert(addAndMerge([1, 2, 3], [4, 5], -1) == [0, 1, 2, 3, 4]); + assert(concatAndAdd([1, 2, 3], [4, 5], -1) == [0, 1, 2, 3, 4]); } /** @@ -125,24 +127,33 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else alias hook = Hook; // } state - // representation + // get /** Returns a copy of the underlying value. */ - auto representation() inout { return payload; } + auto get() inout { return payload; } /// unittest { - auto x = Checked!ubyte(ubyte(42)); - static assert(is(typeof(x.representation()) == ubyte)); - assert(x.representation == 42); + auto x = checked(ubyte(42)); + static assert(is(typeof(x.get()) == ubyte)); + assert(x.get == 42); } /** Defines the minimum and maximum allowed. */ static if (hasMember!(Hook, "min")) + { enum min = Checked!(T, Hook)(Hook.min!T); + /// + unittest + { + assert(Checked!short.min == -32768); + assert(Checked!(short, WithNaN).min == -32767); + assert(Checked!(uint, WithNaN).max == uint.max - 1); + } + } else enum min = Checked(T.min); /// ditto @@ -150,13 +161,6 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) 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` @@ -168,13 +172,21 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) if (valueConvertible!(U, T) || !isIntegral!T && is(typeof(T(rhs))) || is(U == Checked!(V, W), V, W) && - is(typeof(Checked(rhs.representation)))) + is(typeof(Checked(rhs.get)))) { static if (isIntegral!U) payload = rhs; else payload = rhs.payload; } + /// + unittest + { + auto a = Checked!long(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. @@ -186,26 +198,35 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else payload = rhs.payload; } + /// + unittest + { + Checked!long a; + a = 42L; + assert(a == 42); + a = 4242; + assert(a == 4242); + } // opCast /** Casting operator to integral, `bool`, or floating point type. If `Hook` defines `hookOpCast`, the call immediately returns - `hook.hookOpCast!U(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`. + `hook.hookOpCast!U(get)`. Otherwise, casting to `bool` yields $(D + get != 0) and casting to another integral that can represent all + values of `T` returns `get` promoted to `U`. If a cast to a floating-point type is requested and `Hook` defines - `onBadCast`, the cast is verified by ensuring $(D representation == cast(T) - U(representation)). If that is not `true`, - `hook.onBadCast!U(representation)` is returned. + `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 `representation` and $(D cast(U) - representation) are the same arithmetic number. (Note that `int(-1)` and + 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(representation)` is + are not arithmetically equal, `hook.onBadCast!U(get)` is returned. */ @@ -246,7 +267,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /// unittest { - assert(cast(uint) Checked!int(42) == 42); + assert(cast(uint) checked(42) == 42); assert(cast(uint) Checked!(int, WithNaN)(-42) == uint.max); } @@ -254,10 +275,13 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /** 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 representation == rhs) is returned. + hook.hookOpEquals(get, rhs)). Otherwise, the result of the + built-in operation $(D get == rhs) is returned. + + If `U` is also an instance of `Checked`, both hooks (left- and right-hand + side) are introspected for the method `hookOpEquals`. If both define it, + priority is given to the left-hand side. - If `U` is an instance of `Checked` */ bool opEquals(U)(U rhs) if (isIntegral!U || isFloatingPoint!U || is(U == bool) || @@ -294,8 +318,51 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) return payload == rhs; } + /// + version(StdDdoc) unittest + { + static struct MyHook + { + static bool thereWereErrors; + static bool hookOpEquals(L, R)(L lhs, R rhs) + { + if (lhs != rhs) return false; + static if (isUnsigned!L && !isUnsigned!R) + { + if (lhs > 0 && rhs < 0) thereWereErrors = true; + } + else static if (isUnsigned!R && !isUnsigned!L) + if (lhs < 0 && rhs > 0) thereWereErrors = true; + // Preserve built-in behavior. + return true; + } + } + auto a = Checked!(int, MyHook)(-42); + assert(a == uint(-42)); + assert(MyHook.thereWereErrors); + static struct MyHook2 + { + static bool hookOpEquals(L, R)(L lhs, R rhs) + { + return lhs == rhs; + } + } + MyHook.thereWereErrors = false; + assert(Checked!(uint, MyHook2)(uint(-42)) == a); + // Hook on left hand side takes precedence, so no errors + assert(!MyHook.thereWereErrors); + } + // opCmp /** + Compares `this` against `rhs` for ordering. If `Hook` defines `hookOpCmp`, + the function forwards to $(D hook.hookOpEquals(get, rhs)). + Otherwise, the result of the built-in comparison operation is returned. + + If `U` is also an instance of `Checked`, both hooks (left- and right-hand + side) are introspected for the method `hookOpCmp`. If both define it, + priority is given to the left-hand side. + */ auto opCmp(U)(const U rhs) //const pure @safe nothrow @nogc if (isIntegral!U || isFloatingPoint!U || is(U == bool)) @@ -348,8 +415,61 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } } + /// + version(StdDdoc) unittest + { + static struct MyHook + { + static bool thereWereErrors; + static int hookOpCmp(L, R)(L lhs, R rhs) + { + static if (isUnsigned!L && !isUnsigned!R) + { + if (lhs >= 0 && rhs < 0 && rhs >= lhs) + thereWereErrors = true; + } + else static if (isUnsigned!R && !isUnsigned!L) + if (rhs >= 0 && lhs < 0 && lhs >= rhs) + thereWereErrors = true; + // Preserve built-in behavior. + return lhs < rhs ? -1 : lhs > rhs; + } + } + auto a = Checked!(int, 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); + // Hook on left hand side takes precedence, so no errors + assert(!MyHook.thereWereErrors); + } + // opUnary /** + + Defines unary operators `+`, `-`, `~`, `++`, and `--`. Unary `+` is not + overridable and always has built-in behavior (returns `this`). For the + others, if `Hook` defines `hookOpUnary`, `opUnary` forwards to $(D + Checked!(typeof(hook.hookOpUnary!op(get)), + Hook)(hook.hookOpUnary!op(get))). + + If `Hook` does not define `hookOpUnary` but defines `onOverflow`, `opUnary` + forwards to `hook.onOverflow!op(get)` in case an overflow occurs. + For `++` and `--`, the payload is assigned from the result of the call to + `onOverflow`. + + Note that unary `-` is considered to overflow if `T` is a signed integral of + 32 or 64 bits and is equal to the most negative value. This is because that + value has no positive negation. + */ auto opUnary(string op)() if (op == "+" || op == "-" || op == "~") @@ -361,8 +481,8 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) auto r = hook.hookOpUnary!op(payload); return Checked!(typeof(r), Hook)(r); } - else static if (isIntegral!T && !isUnsigned!T && op == "-" && - hasMember!(Hook, "onOverflow")) + else static if (op == "-" && isIntegral!T && T.sizeof >= 4 && + !isUnsigned!T && hasMember!(Hook, "onOverflow")) { import core.checkedint; static assert(is(typeof(-payload) == typeof(payload))); @@ -403,8 +523,39 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) return this; } + /// + version(StdDdoc) 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!(long, MyHook)(long.min); + assert(a == -a); + assert(MyHook.thereWereErrors); + } + // opBinary /** + + Defines binary operators `+`, `-`, `*`, `/`, `%`, `^^`, `&`, `|`, `^`, `<<`, `>>`, + and `>>>`. If `Hook` defines `hookOpBinary`, `opBinary` forwards to $(D + Checked!(typeof(hook.hookOpBinary!op(get, rhs)), + Hook)(hook.hookOpBinary!op(get, rhs))). + + If `Hook` does not define `hookOpBinary` but defines `onOverflow`, + `opBinary` forwards to `hook.onOverflow!op(get, rhs)` in case an + overflow occurs. + + If two `Checked` instances are involved in a binary operation and both + define `hookOpBinary`, the left-hand side hook has priority. If both define + `onOverflow`, a compile-time error occurs. + */ auto opBinary(string op, Rhs)(const Rhs rhs) if (isIntegral!Rhs || isFloatingPoint!Rhs || is(Rhs == bool)) @@ -444,7 +595,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /// ditto auto opBinary(string op, U, Hook1)(Checked!(U, Hook1) rhs) { - alias R = typeof(representation + rhs.payload); + alias R = typeof(get + rhs.payload); static if (valueConvertible!(T, R) && valueConvertible!(U, R) || is(Hook == Hook1)) { @@ -475,12 +626,17 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else { static assert(0, "Conflict between lhs and rhs hooks," ~ - " use .representation on one side to disambiguate."); + " use .get on one side to disambiguate."); } } // opBinaryRight /** + + Defines binary operators `+`, `-`, `*`, `/`, `%`, `^^`, `&`, `|`, `^`, `<<`, + `>>`, and `>>>` for the case when a built-in numeric or Boolean type is on + the left-hand side, and a `Checked` instance is on the right-hand side. + */ auto opBinaryRight(string op, Lhs)(const Lhs lhs) if (isIntegral!Lhs || isFloatingPoint!Lhs || is(Lhs == bool)) @@ -520,6 +676,14 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) // 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, + if `Hook` defines `onBadOpOpAssign` and an overflow occurs, the payload is + assigned from `hook.onBadOpOpAssign!op(payload, Rhs(rhs))`. In all other + cases, the built-in behavior is carried out. */ ref Checked opOpAssign(string op, Rhs)(const Rhs rhs) if (isIntegral!Rhs || isFloatingPoint!Rhs || is(Rhs == bool)) @@ -568,54 +732,166 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } } -// representation +/** + +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`). + +*/ +template checked(Hook = Abort) +{ + Checked!(T, Hook) checked(T)(const T value) + if (is(typeof(Checked!(T, Hook)(value)))) + { + return Checked!(T, Hook)(value); + } +} + +/// +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 unittest { - assert(Checked!(ubyte, void)(ubyte(22)).representation == 22); + assert(Checked!(ubyte, void)(ubyte(22)).get == 22); } // Abort /** -Force all overflows to fail with `assert(0)`. + +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 { - private static void abort(string msg) + private static void abort(A...)(string fmt, A args) { import std.stdio : stderr; - stderr.writeln(msg); + stderr.writefln(fmt, args); import core.stdc.stdlib : abort; - abort(); + abort; } static: + /** + + Called automatically upon a bad cast (one that loses precision or attempts + to convert a negative value to an unsigned type). The source type is `Src` + and the destination type is `Dst`. + + Params: + src = The source of the cast + + Returns: Nominally the result is the desired value of the cast operation, + which will be forwarded as the result of the cast. For `Abort`, the + function never returns because it aborts the program. + + */ Dst onBadCast(Dst, Src)(Src src) { - abort("Bad cast"); + abort("Erroneous cast attempted: cast(%s) %s(%s)", + Dst.stringof, Src.stringof, src); assert(0); } - Lhs onBadOpOpAssign(string x, Lhs, Rhs)(Lhs, Rhs) + + /** + + Called automatically upon a bad `opOpAssign` call (one that loses precision + or attempts to convert a negative value to an unsigned type). + + Params: + x = The binary operator + 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 value 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. + + */ + Lhs onBadOpOpAssign(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) { - abort("Bad opAssign"); + abort("Erroneous assignment attempted: %s(%s) %s= %s(%s)", + Lhs.stringof, lhs, x, Rhs.stringof, rhs); assert(0); } + + /** + + Called automatically upon a bad `opEquals` call (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: 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. + + */ bool onBadOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - abort("Bad comparison for equality"); + abort("Erroneous comparison attempted: %s(%s) == %s(%s)", + Lhs.stringof, lhs, Rhs.stringof, rhs); assert(0); } + + /** + + Called automatically upon a bad `opCmp` call (one that would make a signed + negative value appear greater than or 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: 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. + + */ bool onBadOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - abort("Bad comparison for ordering"); + abort("Erroneous ordering comparison attempted: %s(%s) and %s(%s)", + Lhs.stringof, lhs, Rhs.stringof, rhs); assert(0); } - typeof(~Lhs()) onOverflow(string op, Lhs)(Lhs lhs) + + /** + + Called automatically upon an overflow during a unary or binary operation. + + 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: 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) { - abort("Overflow on unary \"" ~ op ~ "\""); + abort("Overflow on unary operator: %s%s(%s)", x, Lhs.stringof, lhs); assert(0); } - typeof(Lhs() + Rhs()) onOverflow(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) + /// ditto + typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) { - abort("Overflow on binary \"" ~ op ~ "\""); + abort("Overflow on binary operator: %s(%s) %s %s(%s)", + Lhs.stringof, lhs, x, Rhs.stringof, rhs); assert(0); } } @@ -631,13 +907,13 @@ unittest // ProperCompare /** -Implements a 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. +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 @@ -769,19 +1045,51 @@ unittest unittest { auto x1 = Checked!(uint, ProperCompare)(42u); - assert(x1.representation < -1); + 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: - 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); + /** + The default value used for values not explicitly initialized. It is the NaN + value, i.e. `T.min` for signed integrals and `T.max` for unsigned integrals. + */ + enum T defaultValue(T) = T.min == 0 ? T.max : T.min; + /** + The maximum value representable is $(D T.max) for signed integrals, $(D + T.max - 1) for unsigned integrals. The minimum value representable is $(D + T.min + 1) for signed integrals, $(D 0) for unsigned integrals. + */ + enum T max(T) = cast(T) (T.min == 0 ? T.max - 1 : T.max); + /// ditto + enum T min(T) = cast(T) (T.min == 0 ? T(0) : T.min + 1); + + /** + If `rhs` is `WithNaN.defaultValue!Rhs`, returns + `WithNaN.defaultValue!Lhs`. Otherwise, returns $(D cast(Lhs) rhs). + + Params: + rhs = the value being cast (`Rhs` is the first argument to `Checked`) + Lhs = the target type of the cast + + Returns: The result of the cast operation. + */ Lhs hookOpCast(Lhs, Rhs)(Rhs rhs) { static if (is(Lhs == bool)) @@ -806,14 +1114,55 @@ static: return defaultValue!Lhs; } } + + /** + Unconditionally returns `WithNaN.defaultValue!Lhs` for all `opAssign` + failures. + + Params: + x = The operator involved in the `opAssign` operation + Lhs = The target of the assignment (`Lhs` is the first argument to + `Checked`) + Rhs = The right-hand side type in the assignment + + Returns: `WithNaN.defaultValue!Lhs` + */ Lhs onBadOpOpAssign(string x, Lhs, Rhs)(Lhs, Rhs) { return defaultValue!Lhs; } + + /** + + Returns `WithNaN.defaultValue!Lhs` 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; @@ -821,6 +1170,28 @@ static: ? -1.0 : lhs > rhs ? 1.0 : lhs == rhs ? 0.0 : double.init; } + + /** + 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 == "~") @@ -843,11 +1214,30 @@ static: if (v != defaultValue!T) ++v; } } - else static if (x == "-") + else static if (x == "--") { if (v != defaultValue!T) --v; } } + + /** + 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); @@ -859,6 +1249,25 @@ static: } return defaultValue!Result; } + + /** + 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); @@ -870,6 +1279,24 @@ static: } return defaultValue!Result; } + + /** + + 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) @@ -886,15 +1313,15 @@ static: unittest { auto x1 = Checked!(int, WithNaN)(); - assert(x1.representation == int.min); + assert(x1.get == int.min); assert(x1 != x1); assert(!(x1 < x1)); assert(!(x1 > x1)); assert(!(x1 == x1)); ++x1; - assert(x1.representation == int.min); + assert(x1.get == int.min); --x1; - assert(x1.representation == int.min); + assert(x1.get == int.min); x1 = 42; assert(x1 == x1); assert(x1 <= x1); @@ -1137,7 +1564,8 @@ unittest assert(opChecked!"/"(-6, 0u, overflow) == 0 && 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) @@ -1306,7 +1734,7 @@ version(unittest) private struct CountOpBinary assert(x1.hook.calls == 1); assert(x1 << 2 == 42 << 2); assert(x1.hook.calls == 1); - assert(x1 << 42 == x1.representation << x1.representation); + assert(x1 << 42 == x1.get << x1.get); assert(x1.hook.calls == 2); auto x2 = Checked!(int, CountOpBinary)(42); @@ -1394,13 +1822,13 @@ unittest { Checked!(int, void) x; x = 42; - assert(x.representation == 42); + assert(x.get == 42); x = x; - assert(x.representation == 42); + assert(x.get == 42); x = short(43); - assert(x.representation == 43); + assert(x.get == 43); x = ushort(44); - assert(x.representation == 44); + assert(x.get == 44); } unittest @@ -1408,8 +1836,8 @@ 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)).representation == 42); - assert(Checked!(int, void)(ushort(42)).representation == 42); + assert(Checked!(short, void)(short(42)).get == 42); + assert(Checked!(int, void)(ushort(42)).get == 42); } // opCast From 157db49e392fb265f22969f08644a2066dfffc75 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 18 Aug 2016 15:45:50 -0400 Subject: [PATCH 07/26] Workaround bug in libdparse --- std/experimental/checkedint.d | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 2e77b9cbb8a..017770cc269 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -1187,10 +1187,11 @@ static: 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 == + 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`.)) + 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) { From 522077ea3672cccad1dd8df78d03d7bf664698bc Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Fri, 19 Aug 2016 15:52:51 -0400 Subject: [PATCH 08/26] Add Saturate, change definition of onBadOpOpAssign --- std/experimental/checkedint.d | 147 +++++++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 19 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 017770cc269..5425b67a821 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -678,12 +678,25 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /** 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, - if `Hook` defines `onBadOpOpAssign` and an overflow occurs, the payload is - assigned from `hook.onBadOpOpAssign!op(payload, Rhs(rhs))`. In all other - cases, the built-in behavior is carried out. + `<<=`, `>>=`, 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` does not fit in `this` without a loss of data + or a change in sign and if `Hook` defines `onBadOpOpAssign`, the payload is + assigned from `hook.onBadOpOpAssign!T(result)`. + + 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) if (isIntegral!Rhs || isFloatingPoint!Rhs || is(Rhs == bool)) @@ -697,7 +710,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else { alias R = typeof(payload + rhs); - auto r = mixin("this" ~ op ~ "rhs").payload; + auto r = opBinary!op(rhs).payload; static if (valueConvertible!(R, T) || !hasMember!(Hook, "onBadOpOpAssign") || @@ -723,7 +736,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) // Example: uint += ulong const bad = r > max.payload; if (bad) - payload = hook.onBadOpOpAssign!op(payload, Rhs(rhs)); + payload = hook.onBadOpOpAssign!T(r); else payload = cast(T) r; } @@ -807,20 +820,18 @@ static: or attempts to convert a negative value to an unsigned type). Params: - x = The binary operator - 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 value involved in the operator + rhs = The right-hand side value in the assignment, after the operator has + been evaluated 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. */ - Lhs onBadOpOpAssign(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) + Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) { - abort("Erroneous assignment attempted: %s(%s) %s= %s(%s)", - Lhs.stringof, lhs, x, Rhs.stringof, rhs); + abort("Erroneous assignment attempted: %s = %s(%s)", + Lhs.stringof, Rhs.stringof, rhs); assert(0); } @@ -1120,14 +1131,13 @@ static: failures. Params: - x = The operator involved in the `opAssign` operation Lhs = The target of the assignment (`Lhs` is the first argument to `Checked`) Rhs = The right-hand side type in the assignment Returns: `WithNaN.defaultValue!Lhs` */ - Lhs onBadOpOpAssign(string x, Lhs, Rhs)(Lhs, Rhs) + Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs) { return defaultValue!Lhs; } @@ -1341,6 +1351,105 @@ unittest auto x2 = Smart!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: + Lhs = The target of the assignment (`Lhs` is the first argument to + `Checked`) + Rhs = The right-hand side type in the assignment, after the operation has + been computed + + Returns: `Lhs.max` if $(D rhs >= 0), `Lhs.min` otherwise. + + */ + Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) + { + return rhs >= 0 ? Lhs.max : Lhs.min; + } + + /** + + Implements saturation for operators `+`, `-` (unary and binary), `*`, `/`, + `%`, `^^`, `&`, `|`, `^`, `<<`, `>>`, and `>>>`. + + For unary `-`, `onOverflow` is called if $(D lhs == Lhs.min) and `Lhs` is a + signed type. The function returns `Lhs.max`. + + For binary operators, the result is as follows: $(UL $(LI `Lhs.max` if the + result overflows in the positive direction, on division by `0`, or on + shifting right by a negative value) $(LI `Lhs.min` if the result overflows + in the negative direction) $(LI `0` if `lhs` is being shifted left by a + negative value, or shifted right by a large positive value)) + + Params: + x = The operator involved in the `opAssign` operation + Lhs = The left-hand side of the operator (`Lhs` is the first argument to + `Checked`) + Rhs = The right-hand side type in the operator + + Returns: The saturated result of the operator. + + */ + typeof(~Lhs()) onOverflow(string x, Lhs)(Lhs lhs) + { + static assert(x == "-" || x == "++" || x == "--"); + return x == "--" ? Lhs.min : Lhs.max; + } + /// ditto + typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) + { + static if (x == "+") + return rhs >= 0 ? Lhs.max : Lhs.min; + else static if (x == "*") + return (lhs >= 0) == (rhs >= 0) ? Lhs.max : Lhs.min; + else static if (x == "^^") + return lhs & 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 x >= 0 ? Lhs.max : 0; + else static if (x == ">>" || x == ">>>") + return rhs >= 0 ? 0 : Lhs.max; + else + return cast(Lhs) (mixin("lhs" ~ x ~ "rhs")); + } +} + +/// +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" (using terminology from C) to `T2`, where the two are integral types. That is, all of values in `T1` are @@ -1690,10 +1799,10 @@ version(unittest) private struct CountOverflows ++calls; return mixin("lhs" ~ op ~ "rhs"); } - Lhs onBadOpOpAssign(string op, Lhs, Rhs)(Lhs lhs, Rhs rhs) + Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) { ++calls; - return mixin("lhs" ~ op ~ "=rhs"); + return cast(Lhs) rhs; } } From ca9543921f37144785da5f9a6a5852c37f45a992 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Fri, 19 Aug 2016 18:56:23 -0400 Subject: [PATCH 09/26] Add Mumble, improve documentation --- std/experimental/checkedint.d | 223 +++++++++++++++++++++++++++++++--- 1 file changed, 204 insertions(+), 19 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 5425b67a821..ff4eb1ebdad 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -37,7 +37,10 @@ $(UL $(LI $(LREF Abort) fails every incorrect operation with a message to $(REF stderr, std, stdio) followed by a call to `std.core.abort`. It is the default second parameter, i.e. `Checked!short` is the same as $(D Checked!(short, -Abort)). This is also the type returned by the `checked` convenience function.) +Abort))..) + +$(LI $(LREF Mumble) prints incorrect operations to $(REF stderr, std, stdio) but +otherwise preserves the built-in behavior.) $(LI $(LREF ProperCompare) fixes the comparison operators `==`, `!=`, `<`, `<=`, `>`, and `>=` to return correct results in all circumstances, at a slight cost in @@ -49,6 +52,8 @@ of equality.) $(LI $(LREF WithNaN) reserves a special "Not a Number" value.) +$(LI $(LREF Saturate) implements saturating arithmetic.) + ) These policies may be used alone, e.g. $(D Checked!(uint, WithNaN)) defines a @@ -76,6 +81,73 @@ intercepts comparisons before the numbers involved are tested for NaN.) ) +$(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 `onBadOpOpAssign`) $(TD If defined and `hookOpOpAssign` is not +defined, $(D hook.onBadOpOpAssign!op(value)) (where `value` is the value being +assigned) is forwarded to when the result of binary operators `+=`, `-=`, `*=`, + `/=`, `%=`, `^^=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, +and `>>>=` cannot be assigned back to the payload without loss of information or +a change in sign.)) + +) + */ module std.experimental.checkedint; import std.traits : isFloatingPoint, isIntegral, isNumeric, isUnsigned, Unqual; @@ -785,13 +857,6 @@ then abort the program. `Abort` is the default second argument for `Checked`. */ struct Abort { - private static void abort(A...)(string fmt, A args) - { - import std.stdio : stderr; - stderr.writefln(fmt, args); - import core.stdc.stdlib : abort; - abort; - } static: /** @@ -809,8 +874,7 @@ static: */ Dst onBadCast(Dst, Src)(Src src) { - abort("Erroneous cast attempted: cast(%s) %s(%s)", - Dst.stringof, Src.stringof, src); + Mumble.onBadCast!Dst(src); assert(0); } @@ -830,8 +894,7 @@ static: */ Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) { - abort("Erroneous assignment attempted: %s = %s(%s)", - Lhs.stringof, Rhs.stringof, rhs); + Mumble.onBadOpOpAssign!Lhs(rhs); assert(0); } @@ -852,8 +915,7 @@ static: */ bool onBadOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - abort("Erroneous comparison attempted: %s(%s) == %s(%s)", - Lhs.stringof, lhs, Rhs.stringof, rhs); + Mumble.onBadOpEquals(lhs, rhs); assert(0); } @@ -874,8 +936,7 @@ static: */ bool onBadOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - abort("Erroneous ordering comparison attempted: %s(%s) and %s(%s)", - Lhs.stringof, lhs, Rhs.stringof, rhs); + Mumble.onBadOpCmp(lhs, rhs); assert(0); } @@ -895,14 +956,13 @@ static: */ typeof(~Lhs()) onOverflow(string x, Lhs)(Lhs lhs) { - abort("Overflow on unary operator: %s%s(%s)", x, Lhs.stringof, lhs); + Mumble.onOverflow!x(lhs); assert(0); } /// ditto typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) { - abort("Overflow on binary operator: %s(%s) %s %s(%s)", - Lhs.stringof, lhs, x, Rhs.stringof, rhs); + Mumble.onOverflow!x(lhs, rhs); assert(0); } } @@ -915,6 +975,131 @@ unittest //x += long(int.max); } +// Mumble +/** +Hook that prints to `stderr` a trace of all integral errors, without affecting +default behavior. +*/ +struct Mumble +{ + import std.stdio : stderr; +static: + /** + + Called automatically upon a bad cast from `src` to type `Dst` (one that + loses precision or attempts to convert a negative value to an unsigned + type). + + Params: + src = The source of the cast + Dst = The target type of the cast + + Returns: `cast(Dst) src` + + */ + Dst onBadCast(Dst, Src)(Src src) + { + stderr.writefln("Erroneous cast: cast(%s) %s(%s)", + Dst.stringof, Src.stringof, src); + return cast(Dst) src; + } + + /** + + Called automatically upon a bad `opOpAssign` call (one that loses precision + or attempts to convert a negative value to an unsigned type). + + Params: + rhs = The right-hand side value in the assignment, after the operator has + been evaluated + + Returns: `cast(Lhs) rhs` + */ + Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) + { + stderr.writefln("Erroneous assignment: %s = %s(%s)", + Lhs.stringof, Rhs.stringof, rhs); + return cast(Lhs) rhs; + } + + /** + + Called automatically upon a bad `opEquals` call (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: 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. + + */ + bool onBadOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) + { + stderr.writefln("Erroneous comparison: %s(%s) == %s(%s)", + Lhs.stringof, lhs, Rhs.stringof, rhs); + return lhs == rhs; + } + + /** + + Called automatically upon a bad `opCmp` call (one that would make a signed + negative value appear greater than or 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: $(D lhs < rhs ? -1 : lhs > rhs) + + */ + auto onBadOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) + { + stderr.writefln("Erroneous ordering comparison: %s(%s) and %s(%s)", + Lhs.stringof, lhs, Rhs.stringof, rhs); + return lhs < rhs ? -1 : lhs > rhs; + } + + /** + + Called automatically upon an overflow during a unary or binary operation. + + 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: $(D mixin(x ~ "lhs")) for unary, $(D mixin("lhs" ~ x ~ "rhs")) for + binary + + */ + typeof(~Lhs()) onOverflow(string x, Lhs)(ref Lhs lhs) + { + stderr.writefln("Overflow on unary operator: %s%s(%s)", + x, Lhs.stringof, lhs); + return mixin(x ~ "lhs"); + } + /// ditto + typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) + { + stderr.writefln("Overflow on binary operator: %s(%s) %s %s(%s)", + Lhs.stringof, lhs, x, Rhs.stringof, rhs); + return mixin("lhs" ~ x ~ "rhs"); + } +} + +/// +unittest +{ + auto x = checked!Mumble(42); + short x1 = cast(short) x; + //x += long(int.max); +} + // ProperCompare /** From edca085de81923d750d3ebf3ef58f5ba11336f61 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Fri, 19 Aug 2016 19:42:34 -0400 Subject: [PATCH 10/26] Small improvements to documentation --- std/experimental/checkedint.d | 45 ++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index ff4eb1ebdad..fbe9968bb54 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -50,9 +50,14 @@ 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 $(LREF WithNaN) reserves a special "Not a Number" value.) +$(LI $(LREF WithNaN) reserves a special "Not a Number" 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.) -$(LI $(LREF Saturate) implements saturating arithmetic.) +$(LI $(LREF Saturate) 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.) ) @@ -81,6 +86,12 @@ 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)))) @@ -213,11 +224,12 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } /** - Defines the minimum and maximum allowed. + Defines the minimum and maximum. These values are hookable by defining + `Hook.min` and/or `Hook.max`. */ static if (hasMember!(Hook, "min")) { - enum min = Checked!(T, Hook)(Hook.min!T); + enum Checked!(T, Hook) min = Checked!(T, Hook)(Hook.min!T); /// unittest { @@ -227,12 +239,12 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } } else - enum min = Checked(T.min); + enum Checked!(T, Hook) min = Checked(T.min); /// ditto static if (hasMember!(Hook, "max")) - enum max = Checked(Hook.max!T); + enum Checked!(T, Hook) max = Checked(Hook.max!T); else - enum max = Checked(T.max); + enum Checked!(T, Hook) max = Checked(T.max); /** Constructor taking a value properly convertible to the underlying type. `U` @@ -244,7 +256,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) if (valueConvertible!(U, T) || !isIntegral!T && is(typeof(T(rhs))) || is(U == Checked!(V, W), V, W) && - is(typeof(Checked(rhs.get)))) + is(typeof(Checked!(T, Hook)(rhs.get)))) { static if (isIntegral!U) payload = rhs; @@ -254,7 +266,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /// unittest { - auto a = Checked!long(42L); + auto a = checked(42L); assert(a == 42); auto b = Checked!long(4242); // convert 4242 to long assert(b == 4242); @@ -263,7 +275,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /** Assignment operator. Has the same constraints as the constructor. */ - void opAssign(U)(U rhs) if (is(typeof(Checked(rhs)))) + void opAssign(U)(U rhs) if (is(typeof(Checked!(T, Hook)(rhs)))) { static if (isIntegral!U) payload = rhs; @@ -290,8 +302,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) 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. + 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) @@ -340,7 +351,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) unittest { assert(cast(uint) checked(42) == 42); - assert(cast(uint) Checked!(int, WithNaN)(-42) == uint.max); + assert(cast(uint) checked!WithNaN(-42) == uint.max); } // opEquals @@ -409,7 +420,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) return true; } } - auto a = Checked!(int, MyHook)(-42); + auto a = checked!MyHook(-42); assert(a == uint(-42)); assert(MyHook.thereWereErrors); static struct MyHook2 @@ -420,7 +431,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } } MyHook.thereWereErrors = false; - assert(Checked!(uint, MyHook2)(uint(-42)) == a); + assert(checked!MyHook2(uint(-42)) == a); // Hook on left hand side takes precedence, so no errors assert(!MyHook.thereWereErrors); } @@ -507,7 +518,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) return lhs < rhs ? -1 : lhs > rhs; } } - auto a = Checked!(int, MyHook)(-42); + auto a = checked!MyHook(-42); assert(a < uint(-42)); assert(MyHook.thereWereErrors); static struct MyHook2 @@ -607,7 +618,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) return -lhs; } } - auto a = Checked!(long, MyHook)(long.min); + auto a = checked!MyHook(long.min); assert(a == -a); assert(MyHook.thereWereErrors); } From 047687889a490f8ca819b997bb755888f3b725ff Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Sat, 20 Aug 2016 10:25:03 -0400 Subject: [PATCH 11/26] Mumble -> Warn --- std/experimental/checkedint.d | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index fbe9968bb54..02d76210fa5 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -39,7 +39,7 @@ stderr, std, stdio) followed by a call to `std.core.abort`. It is the default second parameter, i.e. `Checked!short` is the same as $(D Checked!(short, Abort))..) -$(LI $(LREF Mumble) prints incorrect operations to $(REF stderr, std, stdio) but +$(LI $(LREF Warn) prints incorrect operations to $(REF stderr, std, stdio) but otherwise preserves the built-in behavior.) $(LI $(LREF ProperCompare) fixes the comparison operators `==`, `!=`, `<`, `<=`, `>`, @@ -885,7 +885,7 @@ static: */ Dst onBadCast(Dst, Src)(Src src) { - Mumble.onBadCast!Dst(src); + Warn.onBadCast!Dst(src); assert(0); } @@ -905,7 +905,7 @@ static: */ Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) { - Mumble.onBadOpOpAssign!Lhs(rhs); + Warn.onBadOpOpAssign!Lhs(rhs); assert(0); } @@ -926,7 +926,7 @@ static: */ bool onBadOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - Mumble.onBadOpEquals(lhs, rhs); + Warn.onBadOpEquals(lhs, rhs); assert(0); } @@ -947,7 +947,7 @@ static: */ bool onBadOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - Mumble.onBadOpCmp(lhs, rhs); + Warn.onBadOpCmp(lhs, rhs); assert(0); } @@ -967,13 +967,13 @@ static: */ typeof(~Lhs()) onOverflow(string x, Lhs)(Lhs lhs) { - Mumble.onOverflow!x(lhs); + Warn.onOverflow!x(lhs); assert(0); } /// ditto typeof(Lhs() + Rhs()) onOverflow(string x, Lhs, Rhs)(Lhs lhs, Rhs rhs) { - Mumble.onOverflow!x(lhs, rhs); + Warn.onOverflow!x(lhs, rhs); assert(0); } } @@ -986,12 +986,12 @@ unittest //x += long(int.max); } -// Mumble +// Warn /** Hook that prints to `stderr` a trace of all integral errors, without affecting default behavior. */ -struct Mumble +struct Warn { import std.stdio : stderr; static: @@ -1106,7 +1106,7 @@ static: /// unittest { - auto x = checked!Mumble(42); + auto x = checked!Warn(42); short x1 = cast(short) x; //x += long(int.max); } From 1a62d66d9393dba8c7f0e847f8a405441c3ba9af Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Tue, 23 Aug 2016 12:17:20 -0400 Subject: [PATCH 12/26] Simplify checked(), replace onBadOpOpAssign with more precise onLowerBound/onUpperBound --- std/experimental/checkedint.d | 132 +++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 57 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 02d76210fa5..2f47ac148fa 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -150,12 +150,15 @@ 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 `onBadOpOpAssign`) $(TD If defined and `hookOpOpAssign` is not -defined, $(D hook.onBadOpOpAssign!op(value)) (where `value` is the value being -assigned) is forwarded to when the result of binary operators `+=`, `-=`, `*=`, - `/=`, `%=`, `^^=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, -and `>>>=` cannot be assigned back to the payload without loss of information or -a change in sign.)) +$(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`.)) ) @@ -258,10 +261,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) is(U == Checked!(V, W), V, W) && is(typeof(Checked!(T, Hook)(rhs.get)))) { - static if (isIntegral!U) - payload = rhs; - else - payload = rhs.payload; + opAssign(rhs); } /// unittest @@ -769,9 +769,11 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) Otherwise, the operator first evaluates $(D auto result = opBinary!op(payload, rhs).payload), which is subject to the hooks in - `opBinary`. Then, if `result` does not fit in `this` without a loss of data - or a change in sign and if `Hook` defines `onBadOpOpAssign`, the payload is - assigned from `hook.onBadOpOpAssign!T(result)`. + `opBinary`. Then, if `result` is less than $(D Checked!(T, Hook).min) and if + `Hook` defines `onLowerBound`, the payload is assigned from $(D + hook.onLowerBound(result, min)). If `result` is greater than $(D Checked!(T, + Hook).max) and if `Hook` defines `onUpperBound`, the payload is assigned + from $(D hook.onUpperBound(result, min)). In all other cases, the built-in behavior is carried out. @@ -781,7 +783,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) Returns: A reference to `this`. */ - ref Checked opOpAssign(string op, Rhs)(const Rhs rhs) + 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)); @@ -794,35 +796,27 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) { alias R = typeof(payload + rhs); auto r = opBinary!op(rhs).payload; + import std.conv : unsigned; - static if (valueConvertible!(R, T) || - !hasMember!(Hook, "onBadOpOpAssign") || - op.among(">>", ">>>")) + static if (ProperCompare.hookOpCmp(R.min, min.payload) < 0 && + hasMember!(Hook, "onLowerBound")) { - // No need to check these - payload = cast(T) r; + if (ProperCompare.hookOpCmp(r, min.get) < 0) + { + payload = hook.onLowerBound(r, min.get); + return this; + } } - else + static if (ProperCompare.hookOpCmp(max.payload, R.max) < 0 && + hasMember!(Hook, "onUpperBound")) { - static if (isUnsigned!T && !isUnsigned!R) + if (ProperCompare.hookOpCmp(r, max.get) > 0) { - // Example: ushort += int - import std.conv : unsigned; - const bad = unsigned(r) > max.payload; + payload = hook.onUpperBound(r, min.get); + return this; } - 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!T(r); - else - payload = cast(T) r; } + payload = cast(T) r; } return this; } @@ -835,13 +829,10 @@ instance by using template argument deduction. The hook type may be specified (by default `Abort`). */ -template checked(Hook = Abort) +Checked!(T, Hook) checked(Hook = Abort, T)(const T value) +if (is(typeof(Checked!(T, Hook)(value)))) { - Checked!(T, Hook) checked(T)(const T value) - if (is(typeof(Checked!(T, Hook)(value)))) - { - return Checked!(T, Hook)(value); - } + return Checked!(T, Hook)(value); } /// @@ -891,8 +882,7 @@ static: /** - Called automatically upon a bad `opOpAssign` call (one that loses precision - or attempts to convert a negative value to an unsigned type). + Called automatically upon a bounds error. Params: rhs = The right-hand side value in the assignment, after the operator has @@ -903,9 +893,15 @@ static: it aborts the program. */ - Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) + T onLowerBound(Rhs, T)(Rhs rhs, T bound) + { + Warn.onLowerBound(rhs, bound); + assert(0); + } + /// ditto + T onUpperBound(Rhs, T)(Rhs rhs, T bound) { - Warn.onBadOpOpAssign!Lhs(rhs); + Warn.onUpperBound(rhs, bound); assert(0); } @@ -1026,11 +1022,18 @@ static: Returns: `cast(Lhs) rhs` */ - Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) + Lhs onLowerBound(Rhs, T)(Rhs rhs, T bound) + { + stderr.writefln("Lower bound error: %s(%s) < %s(%s)", + Rhs.stringof, rhs, T.stringof, bound); + return cast(T) rhs; + } + /// ditto + T onUpperBound(Rhs, T)(Rhs rhs, T bound) { - stderr.writefln("Erroneous assignment: %s = %s(%s)", - Lhs.stringof, Rhs.stringof, rhs); - return cast(Lhs) rhs; + stderr.writefln("Upper bound error: %s(%s) > %s(%s)", + Rhs.stringof, rhs, T.stringof, bound); + return cast(T) rhs; } /** @@ -1260,7 +1263,7 @@ unittest /** 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 +signed integrals, the reserved value is `T.min`. For unsigned integrals, the reserved value is `T.max`. The default value of a $(D Checked!(X, WithNaN)) is its NaN value, so care must @@ -1333,9 +1336,14 @@ static: Returns: `WithNaN.defaultValue!Lhs` */ - Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs) + T onLowerBound(Rhs, T)(Rhs, T) { - return defaultValue!Lhs; + return defaultValue!T; + } + /// ditto + T onUpperBound(Rhs, T)(Rhs, T) + { + return defaultValue!T; } /** @@ -1576,9 +1584,14 @@ static: Returns: `Lhs.max` if $(D rhs >= 0), `Lhs.min` otherwise. */ - Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) + T onLowerBound(Rhs, T)(Rhs rhs, T bound) + { + return bound; + } + /// ditto + T onUpperBound(Rhs, T)(Rhs rhs, T bound) { - return rhs >= 0 ? Lhs.max : Lhs.min; + return bound; } /** @@ -1995,10 +2008,15 @@ version(unittest) private struct CountOverflows ++calls; return mixin("lhs" ~ op ~ "rhs"); } - Lhs onBadOpOpAssign(Lhs, Rhs)(Rhs rhs) + T onLowerBound(Rhs, T)(Rhs rhs, T bound) + { + ++calls; + return cast(T) rhs; + } + T onUpperBound(Rhs, T)(Rhs rhs, T bound) { ++calls; - return cast(Lhs) rhs; + return cast(T) rhs; } } From 77bb316d8e80088f973216e20fe17348fea141f9 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 25 Aug 2016 13:45:32 -0400 Subject: [PATCH 13/26] Adding custom min and max to Checked --- std/experimental/checkedint.d | 133 ++++++++++++++++++++++++---------- 1 file changed, 94 insertions(+), 39 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 2f47ac148fa..965133fbaaa 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -99,8 +99,8 @@ $(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 `min`) $(TD If defined, $(D max(Hook.min!T, minimum)) 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.)) @@ -183,22 +183,25 @@ unittest assert(concatAndAdd([1, 2, 3], [4, 5], -1) == [0, 1, 2, 3, 4]); } +private T representation(T)(T v) if (isIntegral!T) { return v; } + /** Checked integral type wraps an integral `T` and customizes its behavior with the help of a `Hook` type. The type wrapped must be one of the predefined integrals (unqualified), or another instance of `Checked`. */ -struct Checked(T, Hook = Abort) -if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) +struct Checked(T, Hook = Abort, T minimum = T.min, T maximum = T.max) +if (isIntegral!T && is(T == Unqual!T) || + is(T == Checked!(U, H, min1, max1), U, H, alias min1, alias max1)) { import std.algorithm.comparison : among; - import std.traits : hasMember; + import std.traits : hasMember, isIntegral; import std.experimental.allocator.common : stateSize; /** - The type of the integral subject to checking. + The type subject to checking (an alias for the `T` parameter). */ - alias Representation = T; + alias Payload = T; // state { static if (hasMember!(Hook, "defaultValue")) @@ -213,6 +216,17 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else alias hook = Hook; // } state + private auto representation() + { + return payload.representation; + } + + unittest + { + static assert(is(typeof(Checked!(short, Hook)().representation()) + == short)); + } + // get /** Returns a copy of the underlying value. @@ -226,28 +240,35 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) assert(x.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); - /// - unittest - { - assert(Checked!short.min == -32768); - assert(Checked!(short, WithNaN).min == -32767); - assert(Checked!(uint, WithNaN).max == uint.max - 1); - } - } + private enum minRep = + (minimum < Hook.min!T ? Hook.min!T : minimum).representation; else - enum Checked!(T, Hook) min = Checked(T.min); - /// ditto + private enum minRep = minimum.representation; static if (hasMember!(Hook, "max")) - enum Checked!(T, Hook) max = Checked(Hook.max!T); + private enum maxRep = + (maximum > Hook.max!T ? Hook.max!T : maximum).representation; else - enum Checked!(T, Hook) max = Checked(T.max); + private enum maxRep = maximum.representation; + + /** + + Defines the minimum and maximum. If `Hook` defines `min`, then $(D min = + Checked(max(minimum, Hook.min!T))). Otherwise, + + */ + enum Checked min = Checked(minRep); + /// ditto + enum Checked max = Checked(maxRep); + /// + unittest + { + assert(Checked!short.min == -32768); + assert(Checked!(short, WithNaN).min == -32767); + assert(Checked!(uint, WithNaN).max == uint.max - 1); + } + + static assert(minRep <= maxRep); /** Constructor taking a value properly convertible to the underlying type. `U` @@ -258,8 +279,9 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) 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)))) + is(U == Checked!(V, W, min1, max1), V, W, alias min1, alias max1) && + ProperCompare.hookOpCmp(U.minRep, minRep) >= 0 && + ProperCompare.hookOpCmp(U.maxRep, maxRep) <= 0) { opAssign(rhs); } @@ -275,12 +297,32 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /** Assignment operator. Has the same constraints as the constructor. */ - void opAssign(U)(U rhs) if (is(typeof(Checked!(T, Hook)(rhs)))) + void opAssign(U)(U rhs) + if (is(typeof(Checked(rhs)))) { static if (isIntegral!U) - payload = rhs; + alias r = rhs; else - payload = rhs.payload; + auto r = rhs.representation; + static if (ProperCompare.hookOpCmp(r.min, minRep) < 0 && + hasMember!(Hook, "onLowerBound")) + { + if (ProperCompare.hookOpCmp(r, minRep) < 0) + { + payload = hook.onLowerBound(r, minRep); + return; + } + } + static if (ProperCompare.hookOpCmp(maxRep, r.max) < 0 && + hasMember!(Hook, "onUpperBound")) + { + if (ProperCompare.hookOpCmp(r, maxRep) > 0) + { + payload = hook.onUpperBound(r, maxRep); + return; + } + } + payload = r; } /// unittest @@ -368,9 +410,11 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) */ 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))) + is(U == Checked!(V, W, min1, max1), V, W, alias min1, alias max1) && + is(typeof(this == rhs.get))) { - static if (is(U == Checked!(V, W), V, W)) + static if (is(U == Checked!(V, W, min1, max1), V, W, + alias min1, alias max1)) { alias R = typeof(payload + rhs.payload); static if (is(Hook == W)) @@ -472,14 +516,14 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } /// ditto - auto opCmp(U, Hook1)(Checked!(U, Hook1) rhs) + auto opCmp(U, H1, alias min1, alias max1)(Checked!(U, H1, min1, max1) 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)) + else static if (is(Hook == H1)) { // Use the lhs hook return this.opCmp(rhs.payload); @@ -488,7 +532,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) { return hook.hookOpCmp(payload, rhs); } - else static if (hasMember!(Hook1, "hookOpCmp")) + else static if (hasMember!(H1, "hookOpCmp")) { return rhs.hook.hookOpCmp(rhs.payload, this); } @@ -562,6 +606,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) else static if (hasMember!(Hook, "hookOpUnary")) { auto r = hook.hookOpUnary!op(payload); + // Note: we're being conservative here with the min and max return Checked!(typeof(r), Hook)(r); } else static if (op == "-" && isIntegral!T && T.sizeof >= 4 && @@ -572,6 +617,9 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) bool overflow; auto r = negs(payload, overflow); if (overflow) r = hook.onOverflow!op(payload); + //enum min1 = -maxRep, max1 = minRep.min ? maxRep : -minRep; + //return Checked!(T, Hook, min1, max1)(r); + // Note: we're being conservative here with the min and max return Checked(r); } else @@ -645,12 +693,14 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) { alias R = typeof(payload + rhs); static assert(is(typeof(mixin("payload" ~ op ~ "rhs")) == R)); + // Note: being conservative with min and max 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); + // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } else static if (is(Rhs == bool)) @@ -676,7 +726,8 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } /// ditto - auto opBinary(string op, U, Hook1)(Checked!(U, Hook1) rhs) + auto opBinary(string op, U, Hook1, alias min1, alias max1)( + Checked!(U, Hook1, min1, max1) rhs) { alias R = typeof(get + rhs.payload); static if (valueConvertible!(T, R) && valueConvertible!(U, R) || @@ -727,11 +778,13 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) static if (hasMember!(Hook, "hookOpBinaryRight")) { auto r = hook.hookOpBinaryRight!op(lhs, payload); + // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } else static if (hasMember!(Hook, "hookOpBinary")) { auto r = hook.hookOpBinary!op(lhs, payload); + // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } else static if (is(Lhs == bool)) @@ -747,12 +800,14 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) bool overflow; auto r = opChecked!op(lhs, T(payload), overflow); if (overflow) r = hook.onOverflow!op(42); + // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } else { // Default is built-in behavior auto r = mixin("lhs" ~ op ~ "T(payload)"); + // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } } @@ -773,7 +828,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) `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)). + from $(D hook.onUpperBound(result, max)). In all other cases, the built-in behavior is carried out. @@ -1330,7 +1385,7 @@ static: failures. Params: - Lhs = The target of the assignment (`Lhs` is the first argument to + T = The target of the assignment (`T` is the first argument to `Checked`) Rhs = The right-hand side type in the assignment @@ -1576,7 +1631,7 @@ static: not fit in `Lhs` without loss of information or a change in sign. Params: - Lhs = The target of the assignment (`Lhs` is the first argument to + T = The target of the assignment (`T` is the first argument to `Checked`) Rhs = The right-hand side type in the assignment, after the operation has been computed From d7a4ad96c2cc98fa5cdc112fd16fb7f4846936f9 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 25 Aug 2016 14:21:45 -0400 Subject: [PATCH 14/26] Back to no custom min and max --- std/experimental/checkedint.d | 135 ++++++++++------------------------ 1 file changed, 40 insertions(+), 95 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 965133fbaaa..3cf049f8883 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -99,8 +99,8 @@ $(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, $(D max(Hook.min!T, minimum)) is used as the -minimum value 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.)) @@ -183,25 +183,22 @@ unittest assert(concatAndAdd([1, 2, 3], [4, 5], -1) == [0, 1, 2, 3, 4]); } -private T representation(T)(T v) if (isIntegral!T) { return v; } - /** Checked integral type wraps an integral `T` and customizes its behavior with the help of a `Hook` type. The type wrapped must be one of the predefined integrals (unqualified), or another instance of `Checked`. */ -struct Checked(T, Hook = Abort, T minimum = T.min, T maximum = T.max) -if (isIntegral!T && is(T == Unqual!T) || - is(T == Checked!(U, H, min1, max1), U, H, alias min1, alias max1)) +struct Checked(T, Hook = Abort) +if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) { import std.algorithm.comparison : among; - import std.traits : hasMember, isIntegral; + import std.traits : hasMember; import std.experimental.allocator.common : stateSize; /** - The type subject to checking (an alias for the `T` parameter). + The type of the integral subject to checking. */ - alias Payload = T; + alias Representation = T; // state { static if (hasMember!(Hook, "defaultValue")) @@ -216,17 +213,6 @@ if (isIntegral!T && is(T == Unqual!T) || else alias hook = Hook; // } state - private auto representation() - { - return payload.representation; - } - - unittest - { - static assert(is(typeof(Checked!(short, Hook)().representation()) - == short)); - } - // get /** Returns a copy of the underlying value. @@ -240,35 +226,28 @@ if (isIntegral!T && is(T == Unqual!T) || assert(x.get == 42); } - static if (hasMember!(Hook, "min")) - private enum minRep = - (minimum < Hook.min!T ? Hook.min!T : minimum).representation; - else - private enum minRep = minimum.representation; - static if (hasMember!(Hook, "max")) - private enum maxRep = - (maximum > Hook.max!T ? Hook.max!T : maximum).representation; - else - private enum maxRep = maximum.representation; - /** - - Defines the minimum and maximum. If `Hook` defines `min`, then $(D min = - Checked(max(minimum, Hook.min!T))). Otherwise, - + Defines the minimum and maximum. These values are hookable by defining + `Hook.min` and/or `Hook.max`. */ - enum Checked min = Checked(minRep); - /// ditto - enum Checked max = Checked(maxRep); - /// - unittest + static if (hasMember!(Hook, "min")) { - assert(Checked!short.min == -32768); - assert(Checked!(short, WithNaN).min == -32767); - assert(Checked!(uint, WithNaN).max == uint.max - 1); + enum Checked!(T, Hook) min = Checked!(T, Hook)(Hook.min!T); + /// + unittest + { + assert(Checked!short.min == -32768); + assert(Checked!(short, WithNaN).min == -32767); + assert(Checked!(uint, WithNaN).max == uint.max - 1); + } } - - static assert(minRep <= maxRep); + else + enum Checked!(T, Hook) min = Checked(T.min); + /// ditto + static if (hasMember!(Hook, "max")) + enum Checked!(T, Hook) max = Checked(Hook.max!T); + else + enum Checked!(T, Hook) max = Checked(T.max); /** Constructor taking a value properly convertible to the underlying type. `U` @@ -279,9 +258,8 @@ if (isIntegral!T && is(T == Unqual!T) || this(U)(U rhs) if (valueConvertible!(U, T) || !isIntegral!T && is(typeof(T(rhs))) || - is(U == Checked!(V, W, min1, max1), V, W, alias min1, alias max1) && - ProperCompare.hookOpCmp(U.minRep, minRep) >= 0 && - ProperCompare.hookOpCmp(U.maxRep, maxRep) <= 0) + is(U == Checked!(V, W), V, W) && + is(typeof(Checked!(T, Hook)(rhs.get)))) { opAssign(rhs); } @@ -297,32 +275,12 @@ if (isIntegral!T && is(T == Unqual!T) || /** Assignment operator. Has the same constraints as the constructor. */ - void opAssign(U)(U rhs) - if (is(typeof(Checked(rhs)))) + void opAssign(U)(U rhs) if (is(typeof(Checked!(T, Hook)(rhs)))) { static if (isIntegral!U) - alias r = rhs; + payload = rhs; else - auto r = rhs.representation; - static if (ProperCompare.hookOpCmp(r.min, minRep) < 0 && - hasMember!(Hook, "onLowerBound")) - { - if (ProperCompare.hookOpCmp(r, minRep) < 0) - { - payload = hook.onLowerBound(r, minRep); - return; - } - } - static if (ProperCompare.hookOpCmp(maxRep, r.max) < 0 && - hasMember!(Hook, "onUpperBound")) - { - if (ProperCompare.hookOpCmp(r, maxRep) > 0) - { - payload = hook.onUpperBound(r, maxRep); - return; - } - } - payload = r; + payload = rhs.payload; } /// unittest @@ -410,11 +368,9 @@ if (isIntegral!T && is(T == Unqual!T) || */ bool opEquals(U)(U rhs) if (isIntegral!U || isFloatingPoint!U || is(U == bool) || - is(U == Checked!(V, W, min1, max1), V, W, alias min1, alias max1) && - is(typeof(this == rhs.get))) + is(U == Checked!(V, W), V, W) && is(typeof(this == rhs.payload))) { - static if (is(U == Checked!(V, W, min1, max1), V, W, - alias min1, alias max1)) + static if (is(U == Checked!(V, W), V, W)) { alias R = typeof(payload + rhs.payload); static if (is(Hook == W)) @@ -516,14 +472,14 @@ if (isIntegral!T && is(T == Unqual!T) || } /// ditto - auto opCmp(U, H1, alias min1, alias max1)(Checked!(U, H1, min1, max1) rhs) + auto opCmp(U, Hook1)(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 == H1)) + else static if (is(Hook == Hook1)) { // Use the lhs hook return this.opCmp(rhs.payload); @@ -532,7 +488,7 @@ if (isIntegral!T && is(T == Unqual!T) || { return hook.hookOpCmp(payload, rhs); } - else static if (hasMember!(H1, "hookOpCmp")) + else static if (hasMember!(Hook1, "hookOpCmp")) { return rhs.hook.hookOpCmp(rhs.payload, this); } @@ -606,7 +562,6 @@ if (isIntegral!T && is(T == Unqual!T) || else static if (hasMember!(Hook, "hookOpUnary")) { auto r = hook.hookOpUnary!op(payload); - // Note: we're being conservative here with the min and max return Checked!(typeof(r), Hook)(r); } else static if (op == "-" && isIntegral!T && T.sizeof >= 4 && @@ -617,9 +572,6 @@ if (isIntegral!T && is(T == Unqual!T) || bool overflow; auto r = negs(payload, overflow); if (overflow) r = hook.onOverflow!op(payload); - //enum min1 = -maxRep, max1 = minRep.min ? maxRep : -minRep; - //return Checked!(T, Hook, min1, max1)(r); - // Note: we're being conservative here with the min and max return Checked(r); } else @@ -693,14 +645,12 @@ if (isIntegral!T && is(T == Unqual!T) || { alias R = typeof(payload + rhs); static assert(is(typeof(mixin("payload" ~ op ~ "rhs")) == R)); - // Note: being conservative with min and max 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); - // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } else static if (is(Rhs == bool)) @@ -726,8 +676,7 @@ if (isIntegral!T && is(T == Unqual!T) || } /// ditto - auto opBinary(string op, U, Hook1, alias min1, alias max1)( - Checked!(U, Hook1, min1, max1) rhs) + auto opBinary(string op, U, Hook1)(Checked!(U, Hook1) rhs) { alias R = typeof(get + rhs.payload); static if (valueConvertible!(T, R) && valueConvertible!(U, R) || @@ -778,13 +727,11 @@ if (isIntegral!T && is(T == Unqual!T) || static if (hasMember!(Hook, "hookOpBinaryRight")) { auto r = hook.hookOpBinaryRight!op(lhs, payload); - // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } else static if (hasMember!(Hook, "hookOpBinary")) { auto r = hook.hookOpBinary!op(lhs, payload); - // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } else static if (is(Lhs == bool)) @@ -800,14 +747,12 @@ if (isIntegral!T && is(T == Unqual!T) || bool overflow; auto r = opChecked!op(lhs, T(payload), overflow); if (overflow) r = hook.onOverflow!op(42); - // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } else { // Default is built-in behavior auto r = mixin("lhs" ~ op ~ "T(payload)"); - // Note: being conservative with min and max return Checked!(typeof(r), Hook)(r); } } @@ -828,7 +773,7 @@ if (isIntegral!T && is(T == Unqual!T) || `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, max)). + from $(D hook.onUpperBound(result, min)). In all other cases, the built-in behavior is carried out. @@ -1318,7 +1263,7 @@ unittest /** Hook that reserves a special value as a "Not a Number" representative. For -signed integrals, the reserved value is `T.min`. For unsigned integrals, the +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 @@ -1385,7 +1330,7 @@ static: failures. Params: - T = The target of the assignment (`T` is the first argument to + Lhs = The target of the assignment (`Lhs` is the first argument to `Checked`) Rhs = The right-hand side type in the assignment @@ -1631,7 +1576,7 @@ static: not fit in `Lhs` without loss of information or a change in sign. Params: - T = The target of the assignment (`T` is the first argument to + Lhs = The target of the assignment (`Lhs` is the first argument to `Checked`) Rhs = The right-hand side type in the assignment, after the operation has been computed From a09c5a7e4b3f0991753163d62d62953c1546e5d8 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 15 Sep 2016 11:46:57 -0400 Subject: [PATCH 15/26] Add const/immutable support --- std/experimental/checkedint.d | 148 ++++++++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 34 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 3cf049f8883..a2c9de9f1cd 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -189,7 +189,7 @@ help of a `Hook` type. The type wrapped must be one of the predefined integrals (unqualified), or another instance of `Checked`. */ struct Checked(T, Hook = Abort) -if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) +if (isIntegral!T || is(T == Checked!(U, H), U, H)) { import std.algorithm.comparison : among; import std.traits : hasMember; @@ -224,6 +224,9 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) 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); } /** @@ -261,7 +264,10 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) is(U == Checked!(V, W), V, W) && is(typeof(Checked!(T, Hook)(rhs.get)))) { - opAssign(rhs); + static if (isIntegral!U) + payload = rhs; + else + payload = rhs.payload; } /// unittest @@ -313,7 +319,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) returned. */ - U opCast(U)() + U opCast(U, this _)() if (isIntegral!U || isFloatingPoint!U || is(U == bool)) { static if (hasMember!(Hook, "hookOpCast")) @@ -366,7 +372,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) priority is given to the left-hand side. */ - bool opEquals(U)(U rhs) + 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))) { @@ -447,7 +453,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) priority is given to the left-hand side. */ - auto opCmp(U)(const U rhs) //const pure @safe nothrow @nogc + 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")) @@ -472,7 +478,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) } /// ditto - auto opCmp(U, Hook1)(Checked!(U, Hook1) rhs) + auto opCmp(U, Hook1, this _)(Checked!(U, Hook1) rhs) { alias R = typeof(payload + rhs.payload); static if (valueConvertible!(T, R) && valueConvertible!(U, R)) @@ -554,7 +560,7 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) value has no positive negation. */ - auto opUnary(string op)() + auto opUnary(string op, this _)() if (op == "+" || op == "-" || op == "~") { static if (op == "+") @@ -642,6 +648,18 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) */ 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(payload + rhs); static assert(is(typeof(mixin("payload" ~ op ~ "rhs")) == R)); @@ -677,6 +695,18 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) /// 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) || @@ -723,6 +753,18 @@ if (isIntegral!T && is(T == Unqual!T) || is(T == Checked!(U, H), U, H)) */ 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")) { @@ -847,7 +889,13 @@ unittest // get unittest { - assert(Checked!(ubyte, void)(ubyte(22)).get == 22); + void test(T)() + { + assert(Checked!(T, void)(ubyte(22)).get == 22); + } + test!ubyte; + test!(const ubyte); + test!(immutable ubyte); } // Abort @@ -976,10 +1024,17 @@ static: unittest { - Checked!(int, Abort) x; - x = 42; - short x1 = cast(short) x; - //x += long(int.max); + 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); } // Warn @@ -1112,6 +1167,8 @@ unittest auto x = checked!Warn(42); short x1 = cast(short) x; //x += long(int.max); + auto y = checked!Warn(cast(const int) 42); + short y1 = cast(const byte) y; } // ProperCompare @@ -1235,7 +1292,7 @@ unittest assert(opEqualsProper(42, 42u)); assert(-1 == 4294967295u); assert(!opEqualsProper(-1, 4294967295u)); - assert(!opEqualsProper(uint(-1), -1)); + 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)); @@ -1527,22 +1584,45 @@ static: /// unittest { - auto x1 = Checked!(int, WithNaN)(); - assert(x1.get == int.min); - assert(x1 != x1); - assert(!(x1 < x1)); - assert(!(x1 > x1)); - assert(!(x1 == x1)); - ++x1; - assert(x1.get == int.min); - --x1; - assert(x1.get == int.min); - x1 = 42; - assert(x1 == x1); - assert(x1 <= x1); - assert(x1 >= x1); - static assert(x1.min == int.min + 1); - x1 += long(int.max); + void test1(T)() + { + auto x1 = Checked!(T, WithNaN)(); + 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 == 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; } unittest @@ -1552,7 +1632,7 @@ unittest assert(x1 != x1); x1 = -1; assert(x1 < 1u); - auto x2 = Smart!int(42); + auto x2 = Smart!(const int)(42); } // Saturate @@ -1841,7 +1921,7 @@ fail: unittest { bool overflow; - assert(opChecked!"+"(short(1), short(1), overflow) == 2 && !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); @@ -2033,7 +2113,7 @@ version(unittest) private struct CountOpBinary // opBinary @nogc nothrow pure @safe unittest { - auto x = Checked!(int, void)(42), y = Checked!(int, void)(142); + 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); @@ -2404,7 +2484,7 @@ unittest x = 42; assert(x == 42); - short _short = 43; + const short _short = 43; x = _short; assert(x == _short); ushort _ushort = 44; @@ -2420,7 +2500,7 @@ unittest assert(cast(long) x == 44); assert(cast(short) x == 44); - Checked!(uint, void) y; + const Checked!(uint, void) y; assert(y <= y); assert(y == 0); assert(y < x); From 53c9b8b51a62a3e1cfb08140cbe96db8e9110967 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 15 Sep 2016 15:53:47 -0400 Subject: [PATCH 16/26] Review by JohanEngelen --- std/experimental/checkedint.d | 38 ++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index a2c9de9f1cd..2c8862bec13 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -935,6 +935,7 @@ static: 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 @@ -1000,9 +1001,9 @@ static: Called automatically upon an overflow during a unary or binary operation. 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 + 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 @@ -1074,6 +1075,7 @@ static: Params: rhs = The right-hand side value in the assignment, after the operator has been evaluated + bound = The bound being violated Returns: `cast(Lhs) rhs` */ @@ -1138,6 +1140,7 @@ static: 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 @@ -1200,6 +1203,13 @@ struct ProperCompare 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) { @@ -1257,6 +1267,14 @@ struct ProperCompare 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) { @@ -1387,9 +1405,8 @@ static: failures. Params: - Lhs = The target of the assignment (`Lhs` is the first argument to - `Checked`) Rhs = The right-hand side type in the assignment + T = The bound type (value of the bound is not used) Returns: `WithNaN.defaultValue!Lhs` */ @@ -1656,10 +1673,9 @@ static: not fit in `Lhs` without loss of information or a change in sign. Params: - Lhs = The target of the assignment (`Lhs` is the first argument to - `Checked`) 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. @@ -1784,6 +1800,14 @@ $(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 +error = The error 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(L() + R()) opChecked(string x, L, R)(const L lhs, const R rhs, ref bool error) From ca164a7d7445a9a0bb297d60094c6082d4c1e7db Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Fri, 16 Sep 2016 16:51:15 -0400 Subject: [PATCH 17/26] Add isNAN function, fix bug --- std/experimental/checkedint.d | 51 ++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 2c8862bec13..1ae7b33ebca 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -1599,11 +1599,59 @@ static: } /// +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; +} + +/// +unittest +{ + auto x1 = Checked!(int, WithNaN)(); + assert(x1.isNaN); + x1 = 1; + assert(!x1.isNaN); + x1 = x1.init; + assert(x1.isNaN); +} + unittest { void test1(T)() { auto x1 = Checked!(T, WithNaN)(); + assert(x1.isNaN); assert(x1.get == int.min); assert(x1 != x1); assert(!(x1 < x1)); @@ -1611,6 +1659,7 @@ unittest 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); @@ -1869,7 +1918,7 @@ if (isIntegral!L && isIntegral!R) else static if (x == "/" || x == "%") { static if (!isUnsigned!L && !isUnsigned!R && - is(L == Result) && op == "/") + is(L == Result) && x == "/") { if (lhs == Result.min && rhs == -1) goto fail; } From cde67f4ca1bceebd181665fda66b0523aafa0ddc Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Mon, 19 Sep 2016 20:10:29 -0400 Subject: [PATCH 18/26] Increase coverage, fix bugs --- std/experimental/checkedint.d | 133 +++++++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 20 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 1ae7b33ebca..dbc76c25162 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -408,7 +408,7 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } /// - version(StdDdoc) unittest + static if (is(T == int) && is(Hook == void)) unittest { static struct MyHook { @@ -492,11 +492,11 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } else static if (hasMember!(Hook, "hookOpCmp")) { - return hook.hookOpCmp(payload, rhs); + return hook.hookOpCmp(get, rhs.get); } else static if (hasMember!(Hook1, "hookOpCmp")) { - return rhs.hook.hookOpCmp(rhs.payload, this); + return -rhs.hook.hookOpCmp(rhs.payload, get); } else { @@ -505,7 +505,7 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } /// - version(StdDdoc) unittest + static if (is(T == int) && is(Hook == void)) unittest { static struct MyHook { @@ -514,18 +514,20 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) { static if (isUnsigned!L && !isUnsigned!R) { - if (lhs >= 0 && rhs < 0 && rhs >= lhs) + if (rhs < 0 && rhs >= lhs) thereWereErrors = true; } else static if (isUnsigned!R && !isUnsigned!L) - if (rhs >= 0 && lhs < 0 && lhs >= rhs) + { + 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(a > uint(42)); assert(MyHook.thereWereErrors); static struct MyHook2 { @@ -536,9 +538,12 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } } MyHook.thereWereErrors = false; - assert(Checked!(uint, MyHook2)(uint(-42)) == a); + 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); } // opUnary @@ -613,7 +618,7 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } /// - version(StdDdoc) unittest + static if (is(T == int) && is(Hook == void)) unittest { static struct MyHook { @@ -743,6 +748,13 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } } + static if (is(T == int) && is(Hook == void)) unittest + { + const a = checked(42); + assert(a + 1 == 43); + assert(a + checked(uint(42)) == 84); + } + // opBinaryRight /** @@ -836,11 +848,11 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } else { - alias R = typeof(payload + rhs); + alias R = typeof(get + rhs); auto r = opBinary!op(rhs).payload; import std.conv : unsigned; - static if (ProperCompare.hookOpCmp(R.min, min.payload) < 0 && + static if (ProperCompare.hookOpCmp(R.min, min.get) < 0 && hasMember!(Hook, "onLowerBound")) { if (ProperCompare.hookOpCmp(r, min.get) < 0) @@ -849,7 +861,7 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) return this; } } - static if (ProperCompare.hookOpCmp(max.payload, R.max) < 0 && + static if (ProperCompare.hookOpCmp(max.get, R.max) < 0 && hasMember!(Hook, "onUpperBound")) { if (ProperCompare.hookOpCmp(r, max.get) > 0) @@ -862,6 +874,32 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } return this; } + + /// + static if (is(T == int) && is(Hook == void)) 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); + } } /** @@ -1320,6 +1358,10 @@ 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); + assert(opCmpProper(41, double.init) is double.init); assert(opCmpProper(42u, 42) == 0); assert(opCmpProper(42, 42u) == 0); assert(opCmpProper(-1, uint(-1)) < 0); @@ -1387,19 +1429,33 @@ static: } else { - if (isUnsigned!Rhs || !isUnsigned!Lhs || - Rhs.sizeof > Lhs.sizeof || rhs >= 0) + // Not value convertible, only viable option is rhs fits within the + // bounds of Lhs + static if (ProperCompare.hookOpCmp(Rhs.min, Lhs.min) < 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; + // 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 defaultValue!Lhs; + return cast(Lhs) rhs; } } + /// + unittest + { + auto x = checked!WithNaN(422); + assert((cast(ubyte) x) == 255); + x = checked!WithNaN(-422); + assert((cast(byte) x) == -128); + } + /** Unconditionally returns `WithNaN.defaultValue!Lhs` for all `opAssign` failures. @@ -1459,6 +1515,15 @@ static: : lhs > rhs ? 1.0 : lhs == rhs ? 0.0 : double.init; } + /// + 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 `--`. @@ -1509,6 +1574,18 @@ static: } } + /// + unittest + { + Checked!(int, WithNaN) x; + ++x; + assert(x.isNaN); + x = 1; + assert(!x.isNaN); + ++x; + assert(!x.isNaN); + } + /** Defines hooks for binary operators `+`, `-`, `*`, `/`, `%`, `^^`, `&`, `|`, `^`, `<<`, `>>`, and `>>>` for cases where a `Checked` object is the @@ -1596,6 +1673,19 @@ static: ? defaultValue!L : hookOpCast!L(temp); } + + /// + unittest + { + Checked!(int, WithNaN) x; + x += 4; + assert(x.isNaN); + x = 0; + x += 4; + assert(!x.isNaN); + x += int.max; + assert(x.isNaN); + } } /// @@ -2213,6 +2303,9 @@ version(unittest) private struct CountOpBinary 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); From 89df85c412549a3a5d316f4f6cdb6917917e7146 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Mon, 19 Sep 2016 21:41:50 -0400 Subject: [PATCH 19/26] Lil more coverage --- std/experimental/checkedint.d | 3 +++ 1 file changed, 3 insertions(+) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index dbc76c25162..7dce5aa1ca1 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -1454,6 +1454,7 @@ static: assert((cast(ubyte) x) == 255); x = checked!WithNaN(-422); assert((cast(byte) x) == -128); + assert(cast(short) x == -422); } /** @@ -2375,6 +2376,8 @@ unittest 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); From 082b8b61c8935ce2cc8a4cf7aceb3bd35a01f1c6 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Wed, 19 Oct 2016 12:54:50 -0400 Subject: [PATCH 20/26] Add Throw hook, improve coverage, fix a few bugs --- std/experimental/checkedint.d | 567 +++++++++++++++++++++++++++------- 1 file changed, 459 insertions(+), 108 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 7dce5aa1ca1..077fc1ae536 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -35,9 +35,9 @@ This module provides a few predefined hooks (below) that add useful behavior to $(UL $(LI $(LREF Abort) fails every incorrect operation with a message to $(REF -stderr, std, stdio) followed by a call to `std.core.abort`. It is the default +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))..) +Abort)).) $(LI $(LREF Warn) prints incorrect operations to $(REF stderr, std, stdio) but otherwise preserves the built-in behavior.) @@ -429,6 +429,9 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) 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) @@ -444,9 +447,10 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) // opCmp /** + Compares `this` against `rhs` for ordering. If `Hook` defines `hookOpCmp`, - the function forwards to $(D hook.hookOpEquals(get, rhs)). - Otherwise, the result of the built-in comparison operation is returned. + 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, @@ -546,6 +550,14 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) assert(MyHook.thereWereErrors); } + // For coverage + static if (is(T == int) && is(Hook == void)) unittest + { + assert(checked(42) <= checked!void(42)); + assert(checked!void(42) <= checked(42u)); + assert(checked!void(42) <= checked!(void*)(42u)); + } + // opUnary /** @@ -632,6 +644,8 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) auto a = checked!MyHook(long.min); assert(a == -a); assert(MyHook.thereWereErrors); + auto b = checked!void(42); + assert(++b == 43); } // opBinary @@ -666,7 +680,7 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) private auto opBinaryImpl(string op, Rhs, this _)(const Rhs rhs) { - alias R = typeof(payload + 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; @@ -753,6 +767,21 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) 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 @@ -811,6 +840,28 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) } } + static if (is(T == int) && is(Hook == void)) 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 /** @@ -849,7 +900,7 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) else { alias R = typeof(get + rhs); - auto r = opBinary!op(rhs).payload; + auto r = opBinary!op(rhs).get; import std.conv : unsigned; static if (ProperCompare.hookOpCmp(R.min, min.get) < 0 && @@ -857,6 +908,7 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) { if (ProperCompare.hookOpCmp(r, min.get) < 0) { + // Example: Checked!uint(1) += int(-3) payload = hook.onLowerBound(r, min.get); return this; } @@ -866,7 +918,8 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) { if (ProperCompare.hookOpCmp(r, max.get) > 0) { - payload = hook.onUpperBound(r, min.get); + // Example: Checked!uint(1) += long(uint.max) + payload = hook.onUpperBound(r, max.get); return this; } } @@ -994,44 +1047,62 @@ static: /** - Called automatically upon a bad `opEquals` call (one that would make a - signed negative value appear equal to an unsigned positive value). + 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: 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. + Returns: Upon a correct comparison, returns the result of the comparison. + Otherwise, the function terminates the application so it never returns. */ - bool onBadOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) + static bool hookOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - Warn.onBadOpEquals(lhs, rhs); - assert(0); + bool error; + auto result = opChecked!"=="(lhs, rhs, error); + if (error) + { + Warn.hookOpEquals(lhs, rhs); + assert(0); + } + return result; } /** - Called automatically upon a bad `opCmp` call (one that would make a signed - negative value appear greater than or equal to an unsigned positive value). + 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: 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. + 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. */ - bool onBadOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) + int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - Warn.onBadOpCmp(lhs, rhs); - assert(0); + bool error; + auto result = opChecked!"cmp"(lhs, rhs, error); + if (error) + { + Warn.hookOpCmp(lhs, rhs); + assert(0); + } + return result; } /** @@ -1076,6 +1147,192 @@ unittest test!(immutable short); } + +// Throw +/** + +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 Throw +{ + /** + Exception type thrown upon any failure. + */ + static class CheckFailure : Exception + { + this(T...)(string f, T vals) + { + import std.format; + super(format(f, vals)); + } + } + + /** + + Called automatically upon a bad cast (one that loses precision or attempts + to convert a negative value to an unsigned type). The source type is `Src` + and the destination type is `Dst`. + + Params: + src = The source of the cast + + Returns: Nominally the result is the desired value of the cast operation, + which will be forwarded as the result of the cast. For `Throw`, the + function never returns because it throws an exception. + + */ + static Dst onBadCast(Dst, Src)(Src src) + { + throw new CheckFailure("Erroneous cast: cast(%s) %s(%s)", + Dst.stringof, Src.stringof, src); + } + + /** + + Called automatically upon a bounds error. + + Params: + rhs = The right-hand side value in the assignment, after the operator has + been evaluated + bound = The value of the bound being violated + + Returns: Nominally the result is the desired value of the operator, which + will be forwarded as result. For `Abort`, the function never returns because + it aborts the program. + + */ + 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), 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. + + 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 `Abort`, the function never returns because + it aborts the program. + + */ + 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); + } +} + +/// +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(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 @@ -1133,44 +1390,80 @@ static: /** - Called automatically upon a bad `opEquals` call (one that would make a - signed negative value appear equal to an unsigned positive value). + 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: 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. + Returns: In all cases the function returns the built-in result of $(D lhs == + rhs). */ - bool onBadOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) + bool hookOpEquals(Lhs, Rhs)(Lhs lhs, Rhs rhs) + { + bool error; + auto result = opChecked!"=="(lhs, rhs, error); + if (error) + { + stderr.writefln("Erroneous comparison: %s(%s) == %s(%s)", + Lhs.stringof, lhs, Rhs.stringof, rhs); + return lhs == rhs; + } + return result; + } + + /// + unittest { - stderr.writefln("Erroneous comparison: %s(%s) == %s(%s)", - Lhs.stringof, lhs, Rhs.stringof, rhs); - return lhs == rhs; + auto x = checked!Warn(-42); + // Passes + assert(x == -42); + // Passes but prints a warning + // assert(x == uint(-42)); } /** - Called automatically upon a bad `opCmp` call (one that would make a signed - negative value appear greater than or equal to an unsigned positive value). + 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: $(D lhs < rhs ? -1 : lhs > rhs) + Returns: In all cases, returns $(D lhs < rhs ? -1 : lhs > rhs). The result + is not autocorrected in case of an erroneous comparison. */ - auto onBadOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) + int hookOpCmp(Lhs, Rhs)(Lhs lhs, Rhs rhs) { - stderr.writefln("Erroneous ordering comparison: %s(%s) and %s(%s)", - Lhs.stringof, lhs, Rhs.stringof, rhs); - return lhs < rhs ? -1 : lhs > rhs; + bool error; + auto result = opChecked!"cmp"(lhs, rhs, error); + if (error) + { + stderr.writefln("Erroneous ordering comparison: %s(%s) and %s(%s)", + Lhs.stringof, lhs, Rhs.stringof, rhs); + return lhs < rhs ? -1 : lhs > rhs; + } + return result; + } + + /// + unittest + { + auto x = checked!Warn(-42); + // Passes + assert(x <= -42); + // Passes but prints a warning + // assert(x <= uint(-42)); } /** @@ -1267,29 +1560,16 @@ struct ProperCompare 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 + bool error; + auto result = opChecked!"=="(lhs, rhs, error); + if (error) { - if (lhs >= 0) - return true; + // Only possible error is a wrong "true" + return false; } - return false; + return result; } } @@ -1455,32 +1735,16 @@ static: x = checked!WithNaN(-422); assert((cast(byte) x) == -128); assert(cast(short) x == -422); - } - - /** - Unconditionally returns `WithNaN.defaultValue!Lhs` for all `opAssign` - failures. - - Params: - Rhs = The right-hand side type in the assignment - T = The bound type (value of the bound is not used) - - Returns: `WithNaN.defaultValue!Lhs` - */ - T onLowerBound(Rhs, T)(Rhs, T) - { - return defaultValue!T; - } - /// ditto - T onUpperBound(Rhs, T)(Rhs, T) - { - return defaultValue!T; + assert(cast(bool) x); + x = x.init; // set back to NaN + assert(x != true); + assert(x != false); } /** - Returns `WithNaN.defaultValue!Lhs` if $(D lhs == WithNaN.defaultValue!Lhs), - $(D lhs == rhs) otherwise. + 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 @@ -1583,6 +1847,7 @@ static: assert(x.isNaN); x = 1; assert(!x.isNaN); + x = -x; ++x; assert(!x.isNaN); } @@ -1617,6 +1882,15 @@ static: return defaultValue!Result; } + /// + 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 @@ -1646,6 +1920,14 @@ static: } return defaultValue!Result; } + /// + unittest + { + Checked!(int, WithNaN) x; + assert((1 + x).isNaN); + x = 100; + assert(!(1 + x).isNaN); + } /** @@ -1829,6 +2111,15 @@ static: { return bound; } + /// + unittest + { + auto x = checked!Saturate(short(100)); + x += 33000; + assert(x == short.max); + x -= 70000; + assert(x == short.min); + } /** @@ -1866,17 +2157,30 @@ static: else static if (x == "*") return (lhs >= 0) == (rhs >= 0) ? Lhs.max : Lhs.min; else static if (x == "^^") - return lhs & 1 ? Lhs.max : Lhs.min; + 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 x >= 0 ? Lhs.max : 0; + return rhs >= 0 ? Lhs.max : 0; else static if (x == ">>" || x == ">>>") return rhs >= 0 ? 0 : Lhs.max; else - return cast(Lhs) (mixin("lhs" ~ x ~ "rhs")); + static assert(false); + } + /// + unittest + { + assert(checked!Saturate(int.max) + 1 == int.max); + import std.stdio; writeln(checked!Saturate(100) ^^ 10); + 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); } } @@ -1896,32 +2200,16 @@ unittest } /* -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`. +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 ); -*/ -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 - ); - } -} /** @@ -1949,14 +2237,67 @@ error = The error 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(L() + R()) opChecked(string x, L, R)(const L lhs, const R rhs, - ref bool error) +typeof(mixin(x == "cmp" ? "0" : ("L() " ~ x ~ " R()"))) +opChecked(string x, L, R)(const L lhs, const R rhs, ref bool error) if (isIntegral!L && isIntegral!R) { - alias Result = typeof(lhs + rhs); + static if (x == "cmp") + alias Result = int; + else + alias Result = typeof(mixin("L() " ~ x ~ " R()")); + import core.checkedint; import std.algorithm.comparison : among; - static if (x.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; + } + error = 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) + { + error = true; + return -1; + } + if (!isUnsigned!R && rhs < 0) + { + error = 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. @@ -2078,7 +2419,7 @@ if (isIntegral!L && isIntegral!R) debug assert(false); fail: error = true; - return 0; + return Result(0); } /// @@ -2125,6 +2466,10 @@ unittest 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); } /* @@ -2504,6 +2849,12 @@ unittest } } 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); From 0f75fc2ac1b8fe8966978ae39b9b9e2020cf6440 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 20 Oct 2016 11:08:20 -0400 Subject: [PATCH 21/26] Fix copypasta in documentation --- std/experimental/checkedint.d | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 077fc1ae536..e2585adf831 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -1151,8 +1151,9 @@ unittest // Throw /** -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`. +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 @@ -1199,8 +1200,8 @@ struct Throw 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. + will be forwarded as result. For `Throw`, the function never returns because + it throws. */ static T onLowerBound(Rhs, T)(Rhs rhs, T bound) @@ -1248,7 +1249,7 @@ struct Throw 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)`. + 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). @@ -1286,8 +1287,8 @@ struct Throw 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. + will be forwarded as result. For `Throw`, the function never returns because + it throws an exception. */ static typeof(~Lhs()) onOverflow(string x, Lhs)(Lhs lhs) From 30626d289d0ff0e696081b0d20ca9fad5e3d9617 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 20 Oct 2016 11:51:05 -0400 Subject: [PATCH 22/26] Improve coverage a bit more --- std/experimental/checkedint.d | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index e2585adf831..fa11200c93f 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -1625,6 +1625,7 @@ 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); @@ -1853,6 +1854,13 @@ static: assert(!x.isNaN); } + 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 From d3ddf0fd38ece0c0f55c32e250c0592497ae8b51 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Fri, 21 Oct 2016 08:48:57 -0400 Subject: [PATCH 23/26] Improve docs spacing, remove spurious output in unittest --- std/experimental/checkedint.d | 92 +++++++++++++++++------------------ 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index fa11200c93f..92d8a82128f 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -33,32 +33,31 @@ This module provides a few predefined hooks (below) that add useful behavior to `Checked`: $(UL - $(LI $(LREF Abort) 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)).) - +Abort)). +) $(LI $(LREF Warn) prints incorrect operations to $(REF stderr, std, stdio) but -otherwise preserves the built-in behavior.) - +otherwise preserves the built-in behavior. +) $(LI $(LREF 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 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.) - +of equality. +) $(LI $(LREF WithNaN) reserves a special "Not a Number" 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.) - +this special value, it preserves and propagates it until reassigned. +) $(LI $(LREF Saturate) 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.) - +correspondingly overflow toward negative infinity. +) ) These policies may be used alone, e.g. $(D Checked!(uint, WithNaN)) defines a @@ -68,7 +67,6 @@ checked integral emulates an actual integral, which means another checked integral can be built on top of it. Some combinations of interest 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 `Abort` is the default policy.) The order in which policies are combined is @@ -76,14 +74,14 @@ 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.) - +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.) - +intercepts comparisons before the numbers involved are tested for NaN. +) ) The hook's members are looked up statically in a Design by Introspection manner @@ -93,49 +91,48 @@ 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 $(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.)) - +default initializer of the payload.) +) $(TR $(TD `min`) $(TD If defined, `Hook.min!T` is used as the minimum value of -the payload.)) - +the payload.) +) $(TR $(TD `max`) $(TD If defined, `Hook.max!T` is used as the maximum value of -the payload.)) - +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`.)) - +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.)) - +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.)) - +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.)) - +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.)) - +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 `>>>`.)) - +`^^`, `&`, `|`, `^`, `<<`, `>>`, 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 `>>>`.)) - +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 @@ -143,23 +140,23 @@ 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`.)) - +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 `>>>=`.)) - +`^^=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, 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`.)) - +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`.)) - +and `>>>=` is larger than the largest value representable by `T`.) +) ) */ @@ -2182,7 +2179,6 @@ static: unittest { assert(checked!Saturate(int.max) + 1 == int.max); - import std.stdio; writeln(checked!Saturate(100) ^^ 10); assert(checked!Saturate(100) ^^ 10 == int.max); assert(checked!Saturate(-100) ^^ 10 == int.max); assert(checked!Saturate(100) / 0 == int.max); From 591948714085564053fa4967a94c1d6f2b2ab6a8 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Mon, 12 Dec 2016 12:14:06 -0500 Subject: [PATCH 24/26] Use std.math.isNaN instead of comparison to fix failures on Darwin --- std/experimental/checkedint.d | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 92d8a82128f..0619422e044 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -1640,7 +1640,8 @@ unittest assert(opCmpProper(42, 42.0) == 0); assert(opCmpProper(41, 42.0) < 0); assert(opCmpProper(42, 41.0) > 0); - assert(opCmpProper(41, double.init) is double.init); + import std.math; + assert(std.math.isNaN(opCmpProper(41, double.init))); assert(opCmpProper(42u, 42) == 0); assert(opCmpProper(42, 42u) == 0); assert(opCmpProper(-1, uint(-1)) < 0); From 4c9e0502a9b14b409733f99fd21f3ff41e4f55a3 Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Mon, 12 Dec 2016 15:10:32 -0500 Subject: [PATCH 25/26] Please the style checker although I shouldn't have to --- std/experimental/checkedint.d | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/std/experimental/checkedint.d b/std/experimental/checkedint.d index 0619422e044..f2b454ed663 100644 --- a/std/experimental/checkedint.d +++ b/std/experimental/checkedint.d @@ -587,9 +587,9 @@ if (isIntegral!T || is(T == Checked!(U, H), U, H)) else static if (op == "-" && isIntegral!T && T.sizeof >= 4 && !isUnsigned!T && hasMember!(Hook, "onOverflow")) { - import core.checkedint; 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); @@ -1162,7 +1162,7 @@ struct Throw { this(T...)(string f, T vals) { - import std.format; + import std.format : format; super(format(f, vals)); } } @@ -1311,7 +1311,7 @@ unittest auto x1 = cast(T) x; assert(x1 == 42); x = T.max + 1; - import std.exception; + import std.exception : assertThrown, assertNotThrown; assertThrown(cast(T) x); x = x.max; assertThrown(x += 42); @@ -1640,8 +1640,8 @@ unittest assert(opCmpProper(42, 42.0) == 0); assert(opCmpProper(41, 42.0) < 0); assert(opCmpProper(42, 41.0) > 0); - import std.math; - assert(std.math.isNaN(opCmpProper(41, double.init))); + import std.math : isNaN; + assert(isNaN(opCmpProper(41, double.init))); assert(opCmpProper(42u, 42) == 0); assert(opCmpProper(42, 42u) == 0); assert(opCmpProper(-1, uint(-1)) < 0); @@ -2252,7 +2252,7 @@ if (isIntegral!L && isIntegral!R) else alias Result = typeof(mixin("L() " ~ x ~ " R()")); - import core.checkedint; + import core.checkedint : addu, adds, subs, muls, subu, mulu; import std.algorithm.comparison : among; static if (x == "==") { From 855bc64d4ef097e40eef1e9451c013be434633ad Mon Sep 17 00:00:00 2001 From: Andrei Alexandrescu Date: Thu, 15 Dec 2016 10:06:57 -0500 Subject: [PATCH 26/26] Change index.d --- index.d | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.d b/index.d index a318a8e7081..89c95810263 100644 --- a/index.d +++ b/index.d @@ -87,6 +87,10 @@ $(BOOKTABLE , $(TD Compress/decompress data using the zlib library.) ) $(LEADINGROW Data integrity) + $(TR + $(TDNW $(LINK2 std_experimental_checkedint.html, std.experimental.checkedint)) + $(TD Checked integral types.) + ) $(TR $(TDNW $(LINK2 std_digest_crc.html, std.digest.crc)) $(TD Cyclic Redundancy Check (32-bit) implementation.)