Skip to content

fix: fixed-point arithmetic for integer unit conversions (#580)#764

Merged
mpusz merged 13 commits intomasterfrom
fix/fixed-point-integer-conversion
Mar 7, 2026
Merged

fix: fixed-point arithmetic for integer unit conversions (#580)#764
mpusz merged 13 commits intomasterfrom
fix/fixed-point-integer-conversion

Conversation

@mpusz
Copy link
Copy Markdown
Owner

@mpusz mpusz commented Mar 7, 2026

Summary

Core change

Integer-to-integer unit conversions now use a double-width fixed-point representation (detail::fixed_point<T>) for rational scaling factors that are neither integral nor have an integral inverse. This guarantees exact results without promoting to intmax_t or silently overflowing.

Additional changes (made during integration)

Scaling API refactor:

  • Replace ADL scale<M>() free function customization point with scaling_traits<From, To> class template specialization — cleaner and more explicit.
  • Add silent_cast<To>(value) for intentional narrowing casts with float-conversion diagnostics suppressed.
  • Add public is_integral_scaling(from, to) consteval helper so users can write implicitly_scalable specializations without touching get_canonical_unit internals.
  • Remove scaling_kind / get_scaling_kind (over-engineered, only used for one boolean check).
  • Move scaling_overflows_non_zero_values back to detail::.
  • Rename RepConvertibleRepConvertibleFrom, RepAssignableRepAssignableFrom (align with standard preposition convention).

Clang 16 ICE fix:

  • template<UnitMagnitude auto M> inside scaling_traits_impl::scale and user specializations changed to template<auto M> to avoid a clang-16 SIGSEGV in nested concept constraint checking.

Documentation:

  • using_custom_representation_types.md: updated to describe scaling_traits<From, To> specialization pattern.
  • value_conversions.md: updated cross-reference.
  • docs/examples/measurement.md: correct line references and added description of the scaling_traits specialization and the auto M constraint rationale.

burnpanck and others added 13 commits February 28, 2026 22:04
Introduce a fixed-point implementation for unit conversions involving
integer representations, avoiding loss of significant digits that
previously occurred when the conversion factor was not a whole number.

New files:
- src/core/include/mp-units/bits/fixed_point.h: double_width_int<T> and
  fixed_point<T,n> types for exact rational scaling of integer values.
  Uses __int128 when available (__SIZEOF_INT128__) for 64-bit integers.
- src/core/include/mp-units/framework/scaling.h: public scaling_traits<>
  customization point and scale<To>(M, value) free function. Provides
  built-in specializations for floating-point and integer-like types.
- test/static/fixed_point_test.cpp: static assertions for the new types.
- test/runtime/fixed_point_test.cpp: runtime arithmetic edge-case tests.

Modified:
- sudo_cast.h: replace hand-rolled conversion_value_traits / sudo_cast_value
  machinery with a single scale<To::rep>(c_mag, ...) call.
- representation_concepts.h: add MagnitudeScalable concept; replace
  ComplexScalar with HasComplexOperations (which is its definition).
- customization_points.h: add unspecified_rep tag and declare the primary
  scaling_traits<> template.
- framework.h / CMakeLists.txt: wire in the new headers.
- hacks.h: add MP_UNITS_DIAGNOSTIC_IGNORE_PEDANTIC and
  MP_UNITS_DIAGNOSTIC_IGNORE_SIGN_CONVERSION macros.
- example/measurement.cpp: add scaling_traits specializations for
  measurement<T> to demonstrate the customization point.
- test/static/{international,usc}_test.cpp: disable two tests that are
  blocked on issue #614.

Co-authored-by: Tobias Hanhart <burnpanck@users.noreply.github.com>
The partial specialization for types with a nested value_type used
'value_Type' (capital T) instead of 'value_type', making the entire
specialization dead code as the requires-clause could never be satisfied.

Also fix 'mantiassa' -> 'mantissa' in the adjacent comment.
- 'quantitiy' -> 'quantity'
- 'dictatet' -> 'dictated'
- 'convetrible' -> 'convertible'
- 'implemenation' -> 'implementation'
- 'availabe' -> 'available'
When resolving the merge conflict in representation_concepts.h, the
PR's renamed version of the concept ('HasComplexOperations') was used
instead of master's established name ('ComplexScalar'). The two concepts
are semantically equivalent — burnpanck simply renamed it in his branch.

Revert to the canonical 'ComplexScalar' name while retaining the new
'MagnitudeScalable' concept which was the actual addition from the PR.
The PR branched from a version where measurement<T> was defined inline
in measurement.cpp. Master later moved the class to example/include/
measurement.h and changed measurement.cpp to #include that header.

The squash merge therefore introduced a duplicate definition: the class
from the header and the PR's inline class were both visible, causing
an 'ambiguous reference' error. Remove the now-redundant inline class;
the scaling_traits specializations added by the PR work correctly with
the class from measurement.h.
…on all platforms

On ARM / Apple Silicon, long double == double (64-bit mantissa).  The old
fixed_point<T>(long double) initialiser lost ~12 bits of precision for 64-bit
integer types when representing the scaling ratio, producing an error of ~49
units for the 10/9 (degree → gradian) conversion with a 10^18 input value.

Fix by splitting the integer-path else-branch into two cases:

  • Pure rational M (is_integral(M * (denominator(M) / numerator(M))) == true):
    use (value * numerator) / denominator via double_width_int_for_t<> arithmetic.
    This is exact on every platform regardless of long double width.

  • Irrational M (involves π etc.): keep the long double fixed_point approximation.
    These conversions are inherently approximate; small values still produce correct
    truncated results on all platforms.

Update the test comment to reflect the new exact-arithmetic path.

Fixes CI failures on clang-18/ARM and apple-clang-16.
72.27 is not exactly representable as double (it rounds to 72.2699...96).
Multiplying by the conversion factor 100/7227 via long double gives a result
≥ 1.0 on x86 (80-bit long double, 64-bit mantissa) only by chance, but
0.99999...978 on ARM / Apple Silicon where long double == double (52-bit).

The correct mathematical statement is: 7227 tex_point = 100 inch (exact
rational relationship).  Use that integer form instead of the inexact 72.27
double literal so the test is correct and platform-independent.
@mpusz mpusz merged commit efbc844 into master Mar 7, 2026
145 checks passed
@mpusz mpusz deleted the fix/fixed-point-integer-conversion branch March 7, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants