From a28fcce5ab0dfdc6a8911cc9f2500df49d83ef7f Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Wed, 12 Aug 2026 12:20:46 -0400 Subject: [PATCH 1/2] Update overflow and trunctation docs post-Eigen These needed to wait a bit more than the other docs, because the process of writing them uncovered the need for `SameRep` and `cast`. Fixes #70. --- docs/discussion/concepts/overflow.md | 55 ++++++++++++++++++++++++++ docs/discussion/concepts/truncation.md | 45 ++++++++++++++++----- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/docs/discussion/concepts/overflow.md b/docs/discussion/concepts/overflow.md index 9e3302a2..7718ef70 100644 --- a/docs/discussion/concepts/overflow.md +++ b/docs/discussion/concepts/overflow.md @@ -130,6 +130,59 @@ ingredient that lets Au users use a wide variety of integral types with confiden ![The overflow safety surface](../../assets/overflow-safety-surface.png) +#### Integer promotion pitfalls {#integer-promotion} + +There's a key subtlety to appreciate here: the safety surface applies to the _destination_ rep. For +"explicit rep" callsites (e.g., `q.as(unit)`), the destination rep is plain to see (it's `T`). +But "implicit rep" callsites (e.g., `q.as(unit)`) require more care. + +Through Au 0.5.0, the destination rep for an implicit-rep callsite was just the same as the _input_ +rep: if `q` has type `Quantity`, it would be `R`. But we realized this choice was suboptimal +overall: instead, Au should do as C++ does, and return the natural type. [0.6.0] will be the first +release that does so. While it's a better state for the library overall, it does have some +surprising implications. + +As a concrete example, consider `feet(int8_t{20}).as(inches)`. This conversion multiplies the +underlying `int8_t` value by 12. In C++, any arithmetic on an `int8_t` first promotes it to `int` +(this is [integer promotion]), so the result is **an `int`**, not an `int8_t`. Therefore, we get +the wider overflow safety surface for `int`, not the tiny one for `int8_t`, and the conversion is +permitted. + +The conversion itself is perfectly safe, but the real danger comes if you assign back to `int8_t`: + +```cpp +Quantity length_ft = feet(20); + +// Risky conversion: not caught by overflow safety surface! +Quantity length_in = length_ft.as(inches); +``` + +In the second statement, `length_ft.as(inches)` produces a `Quantity` --- note: `int`, +not `int8_t`. This can _implicitly_ convert to `Quantity`, but only because `int` +is the _specific_ type that `int8_t` promotes to. (If we disallowed this implicit conversion, then +integer promotion would make small integer types completely unusable in practice.) And so the +overflow lands silently: 20 feet is 240 inches, which doesn't fit in an `int8_t`, and `length_in` +ends up holding **-16 inches**. + +All these decisions --- returning the promoted type, permitting implicit conversion to the original +type --- are reasonable individually, but taken together, they effectively bypass Au's overflow +protection for small integer types. Fortunately, there's a solution: _name the desired rep when you +do the conversion_. For small integer types, this should almost always be the same rep, so we +provide a convenient syntax ([SameRep]) to ask for this. Continuing with our earlier example: + +```cpp +// Risky conversion caught when output rep specified: +Quantity length_in = length_ft.as(inches); +// Names the rep at the point of conversion ^^^^^^^^^ +``` + +The `` tag lets the conversion machinery know how much room we actually have to work with, +and empowers Au to provide the full safety check. + +Users working with small integer types must already wrestle with their quirky properties in C++. +We recommend these users make this technique a habit whenever they want to stay within the type in +Au operations. + ### Check every conversion at runtime {#check-at-runtime} While the overflow safety surface is a leap forward in safety and flexibility, it's still only @@ -210,3 +263,5 @@ every conversion as it happens, and be prepared for it to fail. [threshold]: https://github.com/aurora-opensource/au/blob/dbd79b2/au/conversion_policy.hh#L27-L28 [#352]: https://github.com/aurora-opensource/au/issues/352 [integer promotion]: https://en.cppreference.com/w/c/language/conversion#Integer_promotions +[SameRep]: ../../reference/quantity.md#same-rep +[0.6.0]: https://github.com/aurora-opensource/au/milestone/9 diff --git a/docs/discussion/concepts/truncation.md b/docs/discussion/concepts/truncation.md index 67056880..6a71e0cf 100644 --- a/docs/discussion/concepts/truncation.md +++ b/docs/discussion/concepts/truncation.md @@ -73,11 +73,23 @@ point representation is a statement, by the user, that _exact values do not matt application. It would be inappropriate for us to raise warnings about perfectly routine properties of the user's chosen type. Hence: _floating point never truncates_. +### Compound types + +Not every rep is a plain arithmetic type. Complex numbers, and Eigen vectors and matrices, are +common examples of _compound_ reps: types built up out of some underlying _scalar_. Au +characterizes each such rep by that scalar --- its +[`ScalarOf`](../../reference/rep.md#scalar-of) --- and then applies the very same rules from +above. So an `Eigen::Vector3d`, whose scalar is `double`, is governed by the [floating point +rule](#float) and never truncates; a vector of integers, on the other hand, follows the integral +rules. + ### Other types -Currently, Au has only limited support for non-arithmetic rep types (full support is tracked in -[#52]). As a stopgap, Au treats any non-arithmetic type conservatively, and assumes that it can -truncate. We hope to refine this approach when we strengthen our support for more rep types. +For a rep that is neither a built-in arithmetic type, nor a compound type with an arithmetic +`ScalarOf` trait, Au has no basis to reason about truncation. Therefore, we fall back to the +conservative assumption that the conversion can truncate, and forbid it by default. This is just +a stopgap measure; we plan to support non-arithmetic reps more broadly in the future. See [#52] to +track any progress. ## Truncation in casting @@ -107,10 +119,23 @@ point as a **non-truncating** operation. ### Non-arithmetic types Here, too, our support for non-arithmetic rep types is limited (see [#52]), and we take -a conservative approach. Any cast involving a non-arithmetic type, either as source or destination, -is considered to have truncation risk, and will not be allowed by default: users must pass -`ignore(TRUNCATION_RISK)` as a second argument to override this. We hope to have better default -behavior once we support non-arithmetic types more fully. +a conservative approach. Any cast involving a rep with non-arithmetic scalar type, either as source +or destination, is considered to have truncation risk, and will not be allowed by default: users +must pass `ignore(TRUNCATION_RISK)` as a second argument to override this. We hope to have better +default behavior once we support non-arithmetic types more fully. + +Note that this policy tells you only whether the _truncation rules_ object to a casting conversion. +Passing those rules doesn't mean the conversion will work: the reps involved must also support the +cast in the first place. + +As a concrete example, `Eigen::Vector3d` is a compound rep that _does_ have an arithmetic scalar +type, so the truncation rules have no objection to casting it to `Eigen::Vector3f`. Even so, the +conversion doesn't compile: Au can't form a common rep for two Eigen types with different scalars, +and so it never gets as far as the `static_cast` it would ordinarily perform. That's no accident on +Eigen's part --- Eigen deliberately rejects `static_cast` between different scalar types, directing +users to its `.cast()` member function instead. So this is the one case where you must reach for +the [`cast` free function](../../reference/eigen.md#cast), which wraps `.cast()` for use with +`Quantity`. ## Summary @@ -122,8 +147,10 @@ whether _individual values_ truncate using the `will_conversion_truncate` functi Truncation risk depends strongly on the types involved. Integral types are vulnerable to truncation for non-integer scale factors. On the other hand, we treat floating point types as though they -never "truncate", because they're already inexact. Finally, since we don't yet fully support -non-arithmetic types, we treat them conservatively and assume that they carry truncation risk. +never "truncate", because they're already inexact. Finally, for compound reps such as complex +numbers and Eigen vectors, we defer to the truncation rules of their underlying scalar type; only +when that scalar can't be determined do we fall back to a conservative assumption of truncation +risk. [overflow]: ./overflow.md [conversion risks]: ./conversion_risks.md From 860ebf124c9efd9e94683c99e4348af56d4297df Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Thu, 13 Aug 2026 11:51:05 -0400 Subject: [PATCH 2/2] Address review feedback --- docs/discussion/concepts/overflow.md | 13 +++++++------ docs/discussion/concepts/truncation.md | 25 ++++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/discussion/concepts/overflow.md b/docs/discussion/concepts/overflow.md index 7718ef70..4b2abf0a 100644 --- a/docs/discussion/concepts/overflow.md +++ b/docs/discussion/concepts/overflow.md @@ -172,16 +172,17 @@ provide a convenient syntax ([SameRep]) to ask for this. Continuing with our ea ```cpp // Risky conversion caught when output rep specified: -Quantity length_in = length_ft.as(inches); -// Names the rep at the point of conversion ^^^^^^^^^ +auto length_in = length_ft.as(inches); +// ^^^^^^^ Names the rep at the point of conversion ``` -The `` tag lets the conversion machinery know how much room we actually have to work with, -and empowers Au to provide the full safety check. +Here, we switched to `auto` for conciseness, because the unit and rep are visibly determined by the +right hand side. The `` tag lets the conversion machinery know how much room we actually +have to work with, and empowers Au to provide the full safety check. Users working with small integer types must already wrestle with their quirky properties in C++. -We recommend these users make this technique a habit whenever they want to stay within the type in -Au operations. +We recommend these users make `SameRep` a habit whenever they want to stay within a promotable +integer type in Au operations. ### Check every conversion at runtime {#check-at-runtime} diff --git a/docs/discussion/concepts/truncation.md b/docs/discussion/concepts/truncation.md index 6a71e0cf..6ad3f13b 100644 --- a/docs/discussion/concepts/truncation.md +++ b/docs/discussion/concepts/truncation.md @@ -75,13 +75,12 @@ of the user's chosen type. Hence: _floating point never truncates_. ### Compound types -Not every rep is a plain arithmetic type. Complex numbers, and Eigen vectors and matrices, are -common examples of _compound_ reps: types built up out of some underlying _scalar_. Au -characterizes each such rep by that scalar --- its -[`ScalarOf`](../../reference/rep.md#scalar-of) --- and then applies the very same rules from -above. So an `Eigen::Vector3d`, whose scalar is `double`, is governed by the [floating point -rule](#float) and never truncates; a vector of integers, on the other hand, follows the integral -rules. +Not every rep is a plain arithmetic type. Complex numbers and Eigen vectors/matrices are common +examples of _compound_ reps: types built up out of some underlying _scalar_. Au characterizes each +such rep by that scalar --- its [`ScalarOf`](../../reference/rep.md#scalar-of) --- and then +applies the very same rules from above. So an `Eigen::Vector3d`, whose scalar is `double`, is +governed by the [floating point rule](#float) and never truncates; a vector of integers, on the +other hand, follows the integral rules. ### Other types @@ -103,7 +102,7 @@ Casting from one _arithmetic_ type to another is governed by simple rules. If the source and destination types are in the same _category_ --- that is, either both integral, or both floating point --- then the cast never truncates. The reason for integral types is straightforward: clearly, the source can't hold a non-integer value. As for floating point types, -they are governed by the [philsophy explained above](#float). +they are governed by the [philosophy explained above](#float). Casting from a floating point type to an integral type does have truncation risk: we would truncate for any non-integer input. We can check this for individual values by discarding the fractional @@ -118,11 +117,11 @@ point as a **non-truncating** operation. ### Non-arithmetic types -Here, too, our support for non-arithmetic rep types is limited (see [#52]), and we take -a conservative approach. Any cast involving a rep with non-arithmetic scalar type, either as source -or destination, is considered to have truncation risk, and will not be allowed by default: users -must pass `ignore(TRUNCATION_RISK)` as a second argument to override this. We hope to have better -default behavior once we support non-arithmetic types more fully. +Here, too, our support for non-arithmetic rep types is limited, and we take a conservative approach. +Any cast involving a rep with non-arithmetic scalar type, either as source or destination, is +considered to have truncation risk, and will not be allowed by default: users must pass +`ignore(TRUNCATION_RISK)` as a second argument to override this. We hope to have better default +behavior once we support non-arithmetic types more fully ([#52]). Note that this policy tells you only whether the _truncation rules_ object to a casting conversion. Passing those rules doesn't mean the conversion will work: the reps involved must also support the