Skip to content

Commit

Permalink
Add most basic runtime conversion checkers (#208)
Browse files Browse the repository at this point in the history
To start out with, this is only for `Quantity` (not `QuantityPoint`),
and only for same-rep conversions (so no explicit-rep variants).  The
plan is to start getting some experience with these, check them out on
godbolt, make some runtime-checking converters that use them, etc.  We
can follow up later with a more complete set of APIs.  Oh, and docs, too
--- this is starting out as an undocumented feature.

Every utility's signature looks something like:

```cpp
constexpr bool is_foo(Quantity q, TargetUnit target);
```

The instances of `is_foo` that we provide are specifically:

- `will_conversion_overflow`
- `will_conversion_truncate`
- `is_conversion_lossy`

The third is the disjunction of the first two.  So we would use them
something like this:

```cpp
constexpr bool ok = is_conversion_lossy(inches(36), feet);
```

The above would return `false`, but would return `true` if we replace
`inches(36)` with `inches(37)`.

On the implementation side, each magnitude-applying type gets its own
dedicated utility.  This is nice, because then the function that checks
each condition appears directly next to the function that applies the
magnitude, so it's easy to check that they're consistent!  (For example,
when applying a rational magnitude to an integral type, we check whether
the _numerator alone_ would cause overflow, because we know we'd be
multiplying by that numerator _before_ dividing by the denominator.)

Along the way, we also make a new type trait to make it easy to get the
appropriate type of magnitude applicator.  This made it a lot easier to
write the tests.

And speaking of tests, we concentrate the vast majority of them in the
`detail`-namespaced helpers, which separately check for overflow and
truncation.  For the `:quantity_test` tests, we just pick one
representative test case that is just complicated enough to give us
confidence that we're correctly using the utilities.

The most exciting test case is the one for `is_conversion_lossy()`.  We
take one type (`uint8_t`), and check for **every representable value**
that a round-trip unit coversion is the identity, _if and only if_ we
have identified that conversion as "not lossy".  We got 100% correct
results for both inches-to-feet (truncation), and feet-to-inches
(overflow).  Nice!
  • Loading branch information
chiphogg authored Dec 11, 2023
1 parent c3061f0 commit 8eaa08f
Show file tree
Hide file tree
Showing 4 changed files with 533 additions and 5 deletions.
112 changes: 107 additions & 5 deletions au/apply_magnitude.hh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,56 @@ constexpr ApplyAs categorize_magnitude(Magnitude<BPs...>) {
template <typename Mag, ApplyAs Category, typename T, bool is_T_integral>
struct ApplyMagnitudeImpl;

template <typename T, bool IsMagnitudeValid>
struct OverflowChecker {
// Default case: `IsMagnitudeValid` is true.
static constexpr bool would_product_overflow(T x, T mag_value) {
return (x > (std::numeric_limits<T>::max() / mag_value)) ||
(x < (std::numeric_limits<T>::lowest() / mag_value));
}
};

template <typename T>
struct OverflowChecker<T, false> {
// Specialization for when `IsMagnitudeValid` is false.
//
// This means that the magnitude itself could not fit inside of the type; therefore, the only
// possible value that would not overflow is zero.
static constexpr bool would_product_overflow(T x, T) { return (x != T{0}); }
};

template <typename T, bool IsTIntegral>
struct TruncationCheckerIfMagnitudeValid {
// Default case: T is integral.
static_assert(std::is_integral<T>::value && IsTIntegral,
"Mismatched instantiation (should never be done manually)");

static constexpr bool would_truncate(T x, T mag_value) { return (x % mag_value != T{0}); }
};

template <typename T>
struct TruncationCheckerIfMagnitudeValid<T, false> {
// Specialization for when T is not integral: by convention, assume no truncation for floats.
static_assert(!std::is_integral<T>::value,
"Mismatched instantiation (should never be done manually)");
static constexpr bool would_truncate(T, T) { return false; }
};

template <typename T, bool IsMagnitudeValid>
// Default case: `IsMagnitudeValid` is true.
struct TruncationChecker : TruncationCheckerIfMagnitudeValid<T, std::is_integral<T>::value> {
static_assert(IsMagnitudeValid, "Mismatched instantiation (should never be done manually)");
};

template <typename T>
struct TruncationChecker<T, false> {
// Specialization for when `IsMagnitudeValid` is false.
//
// This means that the magnitude itself could not fit inside of the type; therefore, the only
// possible value that would not truncate is zero.
static constexpr bool would_truncate(T x, T) { return (x != T{0}); }
};

// Multiplying by an integer, for any type T.
template <typename Mag, typename T, bool is_T_integral>
struct ApplyMagnitudeImpl<Mag, ApplyAs::INTEGER_MULTIPLY, T, is_T_integral> {
Expand All @@ -53,6 +103,14 @@ struct ApplyMagnitudeImpl<Mag, ApplyAs::INTEGER_MULTIPLY, T, is_T_integral> {
"Mismatched instantiation (should never be done manually)");

constexpr T operator()(const T &x) { return x * get_value<T>(Mag{}); }

static constexpr bool would_overflow(const T &x) {
constexpr auto mag_value_result = get_value_result<T>(Mag{});
return OverflowChecker<T, mag_value_result.outcome == MagRepresentationOutcome::OK>::
would_product_overflow(x, mag_value_result.value);
}

static constexpr bool would_truncate(const T &) { return false; }
};

// Dividing by an integer, for any type T.
Expand All @@ -64,6 +122,14 @@ struct ApplyMagnitudeImpl<Mag, ApplyAs::INTEGER_DIVIDE, T, is_T_integral> {
"Mismatched instantiation (should never be done manually)");

constexpr T operator()(const T &x) { return x / get_value<T>(MagInverseT<Mag>{}); }

static constexpr bool would_overflow(const T &) { return false; }

static constexpr bool would_truncate(const T &x) {
constexpr auto mag_value_result = get_value_result<T>(MagInverseT<Mag>{});
return TruncationChecker<T, mag_value_result.outcome == MagRepresentationOutcome::OK>::
would_truncate(x, mag_value_result.value);
}
};

// Applying a (non-integer, non-inverse-integer) rational, for any integral type T.
Expand All @@ -77,6 +143,18 @@ struct ApplyMagnitudeImpl<Mag, ApplyAs::RATIONAL_MULTIPLY, T, true> {
constexpr T operator()(const T &x) {
return x * get_value<T>(numerator(Mag{})) / get_value<T>(denominator(Mag{}));
}

static constexpr bool would_overflow(const T &x) {
constexpr auto mag_value_result = get_value_result<T>(numerator(Mag{}));
return OverflowChecker<T, mag_value_result.outcome == MagRepresentationOutcome::OK>::
would_product_overflow(x, mag_value_result.value);
}

static constexpr bool would_truncate(const T &x) {
constexpr auto mag_value_result = get_value_result<T>(denominator(Mag{}));
return TruncationChecker<T, mag_value_result.outcome == MagRepresentationOutcome::OK>::
would_truncate(x, mag_value_result.value);
}
};

// Applying a (non-integer, non-inverse-integer) rational, for any non-integral type T.
Expand All @@ -88,6 +166,14 @@ struct ApplyMagnitudeImpl<Mag, ApplyAs::RATIONAL_MULTIPLY, T, false> {
"Mismatched instantiation (should never be done manually)");

constexpr T operator()(const T &x) { return x * get_value<T>(Mag{}); }

static constexpr bool would_overflow(const T &x) {
constexpr auto mag_value_result = get_value_result<T>(Mag{});
return OverflowChecker<T, mag_value_result.outcome == MagRepresentationOutcome::OK>::
would_product_overflow(x, mag_value_result.value);
}

static constexpr bool would_truncate(const T &) { return false; }
};

// Applying an irrational for any type T (although only non-integral T makes sense).
Expand All @@ -101,14 +187,30 @@ struct ApplyMagnitudeImpl<Mag, ApplyAs::IRRATIONAL_MULTIPLY, T, is_T_integral> {
"Mismatched instantiation (should never be done manually)");

constexpr T operator()(const T &x) { return x * get_value<T>(Mag{}); }

static constexpr bool would_overflow(const T &x) {
constexpr auto mag_value_result = get_value_result<T>(Mag{});
return OverflowChecker<T, mag_value_result.outcome == MagRepresentationOutcome::OK>::
would_product_overflow(x, mag_value_result.value);
}

static constexpr bool would_truncate(const T &) { return false; }
};

template <typename T, typename MagT>
struct ApplyMagnitudeType;
template <typename T, typename MagT>
using ApplyMagnitudeT = typename ApplyMagnitudeType<T, MagT>::type;
template <typename T, typename... BPs>
struct ApplyMagnitudeType<T, Magnitude<BPs...>>
: stdx::type_identity<ApplyMagnitudeImpl<Magnitude<BPs...>,
categorize_magnitude(Magnitude<BPs...>{}),
T,
std::is_integral<T>::value>> {};

template <typename T, typename... BPs>
constexpr T apply_magnitude(const T &x, Magnitude<BPs...> m) {
return ApplyMagnitudeImpl<Magnitude<BPs...>,
categorize_magnitude(m),
T,
std::is_integral<T>::value>{}(x);
constexpr T apply_magnitude(const T &x, Magnitude<BPs...>) {
return ApplyMagnitudeT<T, Magnitude<BPs...>>{}(x);
}

} // namespace detail
Expand Down
Loading

0 comments on commit 8eaa08f

Please sign in to comment.